create-openclaw-bot 5.10.1 → 5.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,10 +4,10 @@
4
4
 
5
5
  ### One **Web UI** to scaffold, deploy & run free multi-bot AI assistants — zero terminal required
6
6
 
7
- *Run one command → open the dashboard → your bot is live. Windows · macOS · Linux · VPS, on Docker or native.*
7
+ *Run one command → open the dashboard → your bot is live. Windows · macOS · Linux · VPS Docker-powered, auto-installed for you.*
8
8
 
9
9
  <p align="center">
10
- <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.10.1-0EA5E9?style=for-the-badge" alt="Version 5.10.1" /></a>
10
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.11.0-0EA5E9?style=for-the-badge" alt="Version 5.11.0" /></a>
11
11
  <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>
12
12
  <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>
13
13
  <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>
@@ -27,7 +27,14 @@
27
27
 
28
28
  ---
29
29
 
30
- ## 🆕 What's New in v5.10.1
30
+ ## 🆕 What's New in v5.11.0
31
+
32
+ - 🚀 **Facebook Messenger, 1-click**: the `fb-messenger` plugin is now **public on ClawHub** and installs straight from the dashboard — create a Messenger bot, open **Bot → Plugins**, and hit **Install** on the `openclaw-fb-messenger` card. Webhook + Graph API, auto User→Page token, HMAC verify.
33
+ - 🐳 **Docker-only, rock-solid**: dropped the native (non-Docker) install path to focus on the Docker flow that runs flawlessly and stably across Windows / macOS / Linux / VPS, with cross-OS Docker auto-install.
34
+ - 🖥️ **Chrome-debug on headless VPS**: the browser-automation Chrome-debug relay now works on a headless VPS (bridge-IP-scoped, auto-opens ufw); the bot file editor also saves non-`.md` text files.
35
+
36
+ <details>
37
+ <summary><b>Previous: What's new in v5.10.1</b></summary>
31
38
 
32
39
  - 🔧 **Docker fixes**: plugin updates keep the zalo-mod dashboard port, and granted disk/folder mounts (including whole Windows drives like `D:\`) now survive a rebuild instead of being dropped.
33
40
  - 🔄 **Native auto-restart (process supervision)**: native installs now register the gateway + 9router as OS services (macOS launchd, Linux systemd, Windows detached) that restart on crash/reboot — mirroring Docker's `restart: always`. Best-effort, falls back to a plain detached process.
@@ -36,6 +43,8 @@
36
43
  - 🧩 **Config `meta` no longer clobbered**: the generator stops seeding `meta`, so OpenClaw owns it — prevents config parse failures from a version-range `lastTouchedVersion`.
37
44
  - 💅 **Dashboard UI polish**: Bot/Setup hero shortcuts, plugin version badges, cleaner feature-toggle layout, responsive/mobile fixes, and no more duplicate page title on the dashboard tab.
38
45
 
46
+ </details>
47
+
39
48
  <details>
40
49
  <summary><b>Previous: What's new in v5.9.0</b></summary>
41
50
 
@@ -202,7 +211,7 @@ npx github:tuanminhhole/openclaw-setup
202
211
  - **Telegram**: Acquire your official Bot Token from `@BotFather`.
203
212
  - **Zalo Bot API**: Obtain credentials from [developers.zalo.me](https://developers.zalo.me).
204
213
  - **Zalo Personal**: Scan the QR authorization image displayed on the OpenClaw Dashboard.
205
- - **Facebook Messenger**: Via the private `fb-messenger` plugin (contact **tuanminhhole** to receive it) — just provide a Page token.
214
+ - **Facebook Messenger**: Via the `fb-messenger` plugin (public on ClawHub, installable from the Setup UI) — just provide a Page token.
206
215
  - **Discord**: _Coming soon._
207
216
  - **Lark**: _Coming soon._
208
217
 
@@ -287,7 +296,7 @@ Companion repos to build a complete, self-running AI assistant:
287
296
  - [vietbrain](https://github.com/tuanminhhole/vietbrain) — Vietnamese "Second Brain" framework for Obsidian (AI-ready)
288
297
 
289
298
  **🔌 Plugins (runtime)**
290
- - `openclaw-fb-messenger` — Facebook Messenger channel *(private contact to receive)*
299
+ - [openclaw-fb-messenger](https://github.com/tuanminhhole/openclaw-fb-messenger) — Facebook Messenger channel (webhook + Graph API), installable from the Setup UI
291
300
  - [openclaw-telegram-multibot-relay](https://github.com/tuanminhhole/openclaw-telegram-multibot-relay) — Multibot Telegram relay, delegation & native cron reminders
292
301
  - [openclaw-zalo-mod](https://github.com/tuanminhhole/openclaw-zalo-mod) — Zero-token Zalo group management (slash commands, anti-spam, warn, memory)
293
302
  - [openclaw-browser-automation](https://github.com/tuanminhhole/openclaw-browser-automation) — Smart Search & Browser Automation
@@ -295,7 +304,7 @@ Companion repos to build a complete, self-running AI assistant:
295
304
  - [openclaw-n8n-facebook-poster](https://github.com/tuanminhhole/openclaw-n8n-facebook-poster) — Auto-post to Facebook via n8n
296
305
 
297
306
  **🧩 Skills**
298
- - [openclaw-skill-super-memory](https://github.com/tuanminhhole/openclaw-skill-super-memory) — Advanced long-term memory for agents
307
+ - [openclaw-skill-learning-memory](https://github.com/tuanminhhole/openclaw-skill-learning-memory) — Self-evolving long-term memory for agents
299
308
  - [openclaw-skill-infographic](https://github.com/tuanminhhole/openclaw-skill-infographic) — Infographic generation
300
309
  - [openclaw-skill-zalo-sticker-mention](https://github.com/tuanminhhole/openclaw-skill-zalo-sticker-mention) — Stickers & mentions on Zalo
301
310
 
package/README.vi.md CHANGED
@@ -4,10 +4,10 @@
4
4
 
5
5
  ### Một **giao diện Web** để tạo, triển khai & vận hành trợ lý AI đa bot miễn phí — không cần gõ terminal
6
6
 
7
- *Chạy một lệnh → mở dashboard → bot lên sóng. Windows · macOS · Linux · VPS, chạy Docker hoặc native.*
7
+ *Chạy một lệnh → mở dashboard → bot lên sóng. Windows · macOS · Linux · VPS chạy trên Docker, tự cài giúp bạn.*
8
8
 
9
9
  <p align="center">
10
- <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.10.1-0EA5E9?style=for-the-badge" alt="Version 5.10.1" /></a>
10
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.11.0-0EA5E9?style=for-the-badge" alt="Version 5.11.0" /></a>
11
11
  <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>
12
12
  <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>
13
13
  <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>
@@ -27,7 +27,14 @@
27
27
 
28
28
  ---
29
29
 
30
- ## 🆕 Có gì mới trong v5.10.1
30
+ ## 🆕 Có gì mới trong v5.11.0
31
+
32
+ - 🚀 **Facebook Messenger, cài 1 chạm**: plugin `fb-messenger` giờ đã **public trên ClawHub** và cài ngay trên dashboard — tạo bot Messenger, mở **Bot → Plugins**, bấm **Cài** ở thẻ `openclaw-fb-messenger`. Webhook + Graph API, tự đổi User→Page token, xác minh HMAC.
33
+ - 🐳 **Chỉ còn Docker, cực ổn định**: loại bỏ kiểu cài native (không Docker) để tập trung cho luồng Docker chạy hoàn hảo & ổn định trên Windows / macOS / Linux / VPS, kèm tự cài Docker đa hệ điều hành.
34
+ - 🖥️ **Chrome-debug trên VPS headless**: relay Chrome-debug của browser-automation giờ chạy được trên VPS headless (giới hạn theo bridge-IP, tự mở ufw); trình sửa file bot cũng lưu được file text không phải `.md`.
35
+
36
+ <details>
37
+ <summary><b>Trước đó: Có gì mới trong v5.10.1</b></summary>
31
38
 
32
39
  - 🔧 **Sửa lỗi Docker**: update plugin giữ nguyên port dashboard zalo-mod, và ổ đĩa/thư mục đã cấp quyền (kể cả nguyên ổ Windows như `D:\`) không còn bị mất sau khi rebuild.
33
40
  - 🔄 **Tự khởi động lại tiến trình (native)**: cài native giờ đăng ký gateway + 9router thành service hệ điều hành (macOS launchd, Linux systemd, Windows detached), tự chạy lại khi crash/reboot — như `restart: always` của Docker. Best-effort, lỗi thì fallback chạy detached.
@@ -36,6 +43,8 @@
36
43
  - 🧩 **Không ghi đè khối `meta` trong config**: generator ngừng seed `meta` để OpenClaw tự quản — tránh lỗi parse config do `lastTouchedVersion` là dải version.
37
44
  - 💅 **UI dashboard gọn hơn**: nút tắt Bot/Cài đặt ở hero, badge version plugin, layout toggle tính năng, responsive/mobile, bỏ tiêu đề trùng ở tab dashboard.
38
45
 
46
+ </details>
47
+
39
48
  <details>
40
49
  <summary><b>Trước đó: Có gì mới trong v5.9.0</b></summary>
41
50
 
@@ -202,7 +211,7 @@ npx github:tuanminhhole/openclaw-setup
202
211
  - **Telegram**: Lấy token bot chính thức từ `@BotFather`.
203
212
  - **Zalo Bot API**: Lấy thông tin kết nối chính thức từ [developers.zalo.me](https://developers.zalo.me).
204
213
  - **Zalo Cá nhân (Zalo Personal)**: Kích hoạt cực nhanh bằng cách quét mã QR hiển thị ngay trên Dashboard OpenClaw.
205
- - **Facebook Messenger**: Qua plugin riêng `fb-messenger` (liên hệ **tuanminhhole** để nhận) — chỉ cần dùng Page token.
214
+ - **Facebook Messenger**: Qua plugin `fb-messenger` (public trên ClawHub, cài ngay trên giao diện Setup) — chỉ cần dùng Page token.
206
215
  - **Discord**: _Sắp ra mắt._
207
216
  - **Lark**: _Sắp ra mắt._
208
217
 
@@ -287,7 +296,7 @@ Các repo đi kèm để bạn dựng một trợ lý AI "tự vận hành" hoà
287
296
  - [vietbrain](https://github.com/tuanminhhole/vietbrain) — Bộ khung "Bộ Não Thứ Hai" tiếng Việt cho Obsidian (sẵn sàng AI)
288
297
 
289
298
  **🔌 Plugin (runtime)**
290
- - `openclaw-fb-messenger` — Kênh Facebook Messenger *(riêng liên hệ để nhận)*
299
+ - [openclaw-fb-messenger](https://github.com/tuanminhhole/openclaw-fb-messenger) — Kênh Facebook Messenger (webhook + Graph API), cài ngay trên giao diện Setup
291
300
  - [openclaw-telegram-multibot-relay](https://github.com/tuanminhhole/openclaw-telegram-multibot-relay) — Multibot Telegram relay, delegation & cron nhắc lịch native
292
301
  - [openclaw-zalo-mod](https://github.com/tuanminhhole/openclaw-zalo-mod) — Quản lý nhóm Zalo zero-token (slash command, anti-spam, warn, memory)
293
302
  - [openclaw-browser-automation](https://github.com/tuanminhhole/openclaw-browser-automation) — Smart Search & Browser Automation
@@ -295,7 +304,7 @@ Các repo đi kèm để bạn dựng một trợ lý AI "tự vận hành" hoà
295
304
  - [openclaw-n8n-facebook-poster](https://github.com/tuanminhhole/openclaw-n8n-facebook-poster) — Tự động đăng Facebook qua n8n
296
305
 
297
306
  **🧩 Skill**
298
- - [openclaw-skill-super-memory](https://github.com/tuanminhhole/openclaw-skill-super-memory) — Bộ nhớ nâng cao cho agent
307
+ - [openclaw-skill-learning-memory](https://github.com/tuanminhhole/openclaw-skill-learning-memory) — Bộ nhớ dài hạn tự tiến hoá cho agent
299
308
  - [openclaw-skill-infographic](https://github.com/tuanminhhole/openclaw-skill-infographic) — Tạo infographic
300
309
  - [openclaw-skill-zalo-sticker-mention](https://github.com/tuanminhhole/openclaw-skill-zalo-sticker-mention) — Sticker & mention trên Zalo
301
310
 
@@ -388,9 +388,9 @@ async function getAvailableDrives() {
388
388
  return drives.length ? drives : ['C:\\', 'D:\\'];
389
389
  }
390
390
 
391
- function recommendedMode(osChoice) {
392
- if (osChoice === 'win' || osChoice === 'macos') return 'docker';
393
- return 'native';
391
+ // Docker is the only supported deploy mode now (native was removed).
392
+ function recommendedMode() {
393
+ return 'docker';
394
394
  }
395
395
 
396
396
  function commandExists(cmd, args = ['--version']) {
@@ -438,27 +438,6 @@ function run(cmd, args, opts = {}) {
438
438
  });
439
439
  }
440
440
 
441
- function startDetached(cmd, args, opts = {}) {
442
- sendLog(`$ ${cmd} ${args.join(' ')} &`);
443
- const shell = process.platform === 'win32';
444
- const rawBin = resolveBinPath(cmd);
445
- const bin = shell && rawBin.includes(' ') && !rawBin.startsWith('"') ? `"${rawBin}"` : rawBin;
446
- const child = spawn(bin, args, {
447
- cwd: opts.cwd,
448
- shell,
449
- detached: true,
450
- stdio: 'ignore',
451
- windowsHide: opts.windowsHide ?? true,
452
- env: { ...process.env, ...(opts.env || {}) },
453
- });
454
- child.on('error', (err) => {
455
- sendLog(`[error] Failed to start background command "${cmd}": ${err.message}`);
456
- console.error(`Failed to start background command "${cmd}":`, err);
457
- });
458
- child.unref();
459
- return child.pid;
460
- }
461
-
462
441
  async function getCurrentRuntimeVersions() {
463
442
  const [openclaw, nineRouter, node] = await Promise.all([
464
443
  commandExists('openclaw', ['--version']),
@@ -605,135 +584,6 @@ Timed out after ${opts.timeout}ms`.trim() : stderr });
605
584
  });
606
585
  }
607
586
 
608
- // ── Native process supervision (auto-restart) ───────────────────────────────
609
- // Docker containers get `restart: always` for free. For native installs we register the gateway
610
- // and 9router as OS services so they survive crashes and reboots, mirroring that guarantee:
611
- // • macOS → per-user launchd LaunchAgent (KeepAlive + RunAtLoad)
612
- // • Linux → systemd unit (system unit when root, else --user) with Restart=always
613
- // • Windows / anything else → plain detached process (no supervision; unchanged behaviour)
614
- // Every step is best-effort: any failure falls back to startDetached, so a native install can
615
- // never end up worse off than before this feature existed.
616
- function escapeXml(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
617
- function nativeServiceId(projectDir) { return slugify(basename(projectDir || 'openclaw')) || 'bot'; }
618
- function nativeServiceLabels(projectDir) {
619
- const id = nativeServiceId(projectDir);
620
- return {
621
- '9router': { launchd: `com.openclaw.9router.${id}`, systemd: `openclaw-9router-${id}.service` },
622
- sync: { launchd: `com.openclaw.sync.${id}`, systemd: `openclaw-sync-${id}.service` },
623
- gateway: { launchd: `com.openclaw.gateway.${id}`, systemd: `openclaw-gateway-${id}.service` },
624
- };
625
- }
626
- async function resolveAbsoluteBin(cmd) {
627
- try {
628
- const out = await runCapture(process.platform === 'win32' ? 'where' : 'which', [cmd], { shell: false, timeout: 5000 });
629
- const first = String(out.stdout || '').split(/\r?\n/).map((s) => s.trim()).find(Boolean);
630
- if (out.code === 0 && first) return first;
631
- } catch {}
632
- return resolveBinPath(cmd);
633
- }
634
- function systemdUserUnitDir() { return join(getRealHomedir(), '.config', 'systemd', 'user'); }
635
- function isRootUser() { return typeof process.getuid === 'function' && process.getuid() === 0; }
636
-
637
- /**
638
- * Start one native process under OS supervision (auto-restart). Falls back to a plain detached
639
- * process on Windows/unknown platforms or if the service tooling errors. Returns the method used:
640
- * 'launchd' | 'systemd' | 'detached'.
641
- */
642
- async function startNativeService({ projectDir, name, cmd, args, desc, env }) {
643
- const labels = nativeServiceLabels(projectDir)[name];
644
- const logDir = join(projectDir, '.openclaw', 'logs');
645
- await fsp.mkdir(logDir, { recursive: true }).catch(() => {});
646
- const serviceEnv = { PATH: process.env.PATH || '', HOME: getRealHomedir(), ...env };
647
- try {
648
- const absBin = await resolveAbsoluteBin(cmd);
649
- if (process.platform === 'darwin') {
650
- const agentsDir = join(getRealHomedir(), 'Library', 'LaunchAgents');
651
- await fsp.mkdir(agentsDir, { recursive: true });
652
- const progArgs = [absBin, ...args].map((a) => ` <string>${escapeXml(a)}</string>`).join('\n');
653
- const envDict = Object.entries(serviceEnv).map(([k, v]) => ` <key>${escapeXml(k)}</key><string>${escapeXml(String(v))}</string>`).join('\n');
654
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
655
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
656
- <plist version="1.0"><dict>
657
- <key>Label</key><string>${labels.launchd}</string>
658
- <key>ProgramArguments</key><array>
659
- ${progArgs}
660
- </array>
661
- <key>WorkingDirectory</key><string>${escapeXml(projectDir)}</string>
662
- <key>EnvironmentVariables</key><dict>
663
- ${envDict}
664
- </dict>
665
- <key>RunAtLoad</key><true/>
666
- <key>KeepAlive</key><true/>
667
- <key>StandardOutPath</key><string>${escapeXml(join(logDir, name + '.out.log'))}</string>
668
- <key>StandardErrorPath</key><string>${escapeXml(join(logDir, name + '.err.log'))}</string>
669
- </dict></plist>`;
670
- const plistPath = join(agentsDir, `${labels.launchd}.plist`);
671
- await fsp.writeFile(plistPath, plist, 'utf8');
672
- await run('launchctl', ['unload', plistPath], {}).catch(() => {});
673
- await run('launchctl', ['load', '-w', plistPath], {});
674
- sendLog(`[native] launchd service ${labels.launchd} loaded (auto-restart)`);
675
- return 'launchd';
676
- }
677
- if (process.platform === 'linux') {
678
- const root = isRootUser();
679
- const unitDir = root ? '/etc/systemd/system' : systemdUserUnitDir();
680
- await fsp.mkdir(unitDir, { recursive: true });
681
- const sc = (a) => run('systemctl', root ? a : ['--user', ...a], {});
682
- const envLines = Object.entries(serviceEnv).map(([k, v]) => `Environment="${k}=${String(v).replace(/\n/g, ' ')}"`).join('\n');
683
- const execStart = [absBin, ...args].map((a) => (/[\s"']/.test(a) ? JSON.stringify(a) : a)).join(' ');
684
- const unit = `[Unit]
685
- Description=${desc}
686
- After=network-online.target
687
- Wants=network-online.target
688
-
689
- [Service]
690
- Type=simple
691
- WorkingDirectory=${projectDir}
692
- ${envLines}
693
- ExecStart=${execStart}
694
- Restart=always
695
- RestartSec=3
696
-
697
- [Install]
698
- WantedBy=${root ? 'multi-user.target' : 'default.target'}
699
- `;
700
- await fsp.writeFile(join(unitDir, labels.systemd), unit, 'utf8');
701
- await sc(['daemon-reload']);
702
- await sc(['enable', '--now', labels.systemd]);
703
- sendLog(`[native] systemd ${root ? 'system' : 'user'} service ${labels.systemd} enabled (Restart=always)`);
704
- return 'systemd';
705
- }
706
- } catch (e) {
707
- sendLog(`[native] ${name} service setup failed (${e.message}); falling back to detached process`);
708
- }
709
- const pid = startDetached(cmd, args, { cwd: projectDir, env });
710
- if (name === 'gateway') state.botPid = pid;
711
- return 'detached';
712
- }
713
-
714
- /** Remove any launchd/systemd services registered for a native project (used on delete). */
715
- async function removeNativeAutostart(projectDir) {
716
- const labels = nativeServiceLabels(projectDir);
717
- try {
718
- if (process.platform === 'darwin') {
719
- const agentsDir = join(getRealHomedir(), 'Library', 'LaunchAgents');
720
- for (const k of ['9router', 'sync', 'gateway']) {
721
- const p = join(agentsDir, `${labels[k].launchd}.plist`);
722
- if (existsSync(p)) { await run('launchctl', ['unload', p], {}).catch(() => {}); await fsp.unlink(p).catch(() => {}); }
723
- }
724
- } else if (process.platform === 'linux') {
725
- const root = isRootUser();
726
- const unitDir = root ? '/etc/systemd/system' : systemdUserUnitDir();
727
- const sc = (a) => run('systemctl', root ? a : ['--user', ...a], {}).catch(() => {});
728
- let changed = false;
729
- for (const k of ['9router', 'sync', 'gateway']) {
730
- const u = join(unitDir, labels[k].systemd);
731
- if (existsSync(u)) { await sc(['disable', '--now', labels[k].systemd]); await fsp.unlink(u).catch(() => {}); changed = true; }
732
- }
733
- if (changed) await sc(['daemon-reload']);
734
- }
735
- } catch (e) { sendLog(`[native] autostart teardown skipped: ${e.message}`); }
736
- }
737
587
 
738
588
  function safeJoin(root, name) {
739
589
  const clean = normalize(String(name || '')).replace(/^([/\\])+/, '');
@@ -2079,104 +1929,7 @@ try{
2079
1929
  return { message: 'Generating Zalo QR. The image will appear automatically.' };
2080
1930
  }
2081
1931
 
2082
- // ── Native (non-Docker) path ──────────────────────────────────────────────
2083
- // Same flow as Docker but on the host: run `openclaw channels login` directly,
2084
- // read the QR PNG straight off the host filesystem, then restart the native
2085
- // gateway so the saved credentials take effect.
2086
- const gatewayPort = state.gatewayPort || 18789;
2087
- const runtimeEnv = {
2088
- ...process.env,
2089
- OPENCLAW_HOME: join(projectDir, '.openclaw'),
2090
- OPENCLAW_STATE_DIR: join(projectDir, '.openclaw'),
2091
- OPENCLAW_GATEWAY_PORT: String(gatewayPort),
2092
- OPENCLAW_PORT: String(gatewayPort),
2093
- };
2094
-
2095
- // The runtime writes the QR to a uid-scoped temp dir; probe the likely locations.
2096
- const uid = (typeof process.getuid === 'function') ? process.getuid() : '';
2097
- const qrNames = profile === 'default'
2098
- ? ['openclaw-zalouser-qr.png', 'openclaw-zalouser-qr-default.png', `openclaw-zalouser-qr-${profile}.png`]
2099
- : [`openclaw-zalouser-qr-${profile}.png`];
2100
- const qrDirs = [join(os.tmpdir(), 'openclaw'), '/tmp/openclaw', `/tmp/openclaw-${uid}`, '/tmp/openclaw-1000', join(os.tmpdir(), `openclaw-${uid}`)];
2101
- const nativeQrPaths = [...new Set(qrDirs.flatMap((d) => qrNames.map((n) => join(d, n))))];
2102
-
2103
- // Clean stale credentials & QR files so we only ever surface a fresh code.
2104
- const credFile = profile === 'default' ? 'credentials.json' : `credentials-${profile}.json`;
2105
- const credPath = join(projectDir, '.openclaw', 'credentials', 'zalouser', credFile);
2106
- try { await fsp.rm(credPath, { force: true }); } catch {}
2107
- for (const p of nativeQrPaths) { try { await fsp.rm(p, { force: true }); } catch {} }
2108
-
2109
- sendLog('[zalouser] Generating Zalo QR (native). The image will appear automatically.');
2110
-
2111
- const MAX_LOGIN_ATTEMPTS = 4;
2112
- const RETRY_DELAYS = [0, 8000, 15000, 20000];
2113
- let loginAttempt = 0;
2114
- let sent = false;
2115
- let restartAfterLogin = false;
2116
-
2117
- // Poll the host filesystem for the QR PNG across all retry attempts.
2118
- let tries = 0;
2119
- const poll = setInterval(() => {
2120
- if (sent || tries++ > 120) {
2121
- clearInterval(poll);
2122
- if (!sent) sendLog('[zalouser] QR not found yet. Try closing/reopening login, or run `openclaw channels login --channel zalouser --verbose` on the host.');
2123
- return;
2124
- }
2125
- for (const p of nativeQrPaths) {
2126
- try {
2127
- if (existsSync(p) && fs.statSync(p).size > 100) {
2128
- const b64 = fs.readFileSync(p).toString('base64');
2129
- if (b64.length > 100) {
2130
- sent = true;
2131
- clearInterval(poll);
2132
- sendLog(`[zalouser:qr] data:image/png;base64,${b64}`);
2133
- sendLog('[zalouser] Scan this QR with the Zalo app.');
2134
- break;
2135
- }
2136
- }
2137
- } catch {}
2138
- }
2139
- }, 1000);
2140
-
2141
- const loginArgs = ['channels', 'login', '--channel', 'zalouser', '--account', profile, '--verbose'];
2142
- const runLoginAttempt = () => {
2143
- loginAttempt++;
2144
- if (loginAttempt > 1) sendLog(`[zalouser] Retry attempt ${loginAttempt}/${MAX_LOGIN_ATTEMPTS}...`);
2145
- const child = spawn('openclaw', loginArgs, { cwd: projectDir, env: runtimeEnv, shell: false, windowsHide: true });
2146
- const handleLoginLine = (line) => {
2147
- sendLog(line);
2148
- if (/login successful|saved auth/i.test(line)) restartAfterLogin = true;
2149
- };
2150
- child.stdout.on('data', (d) => String(d).split(/\r?\n/).filter(Boolean).forEach(handleLoginLine));
2151
- child.stderr.on('data', (d) => String(d).split(/\r?\n/).filter(Boolean).forEach(handleLoginLine));
2152
- child.on('error', (err) => sendLog(`[zalouser] Login process failed: ${err.message}. Is the 'openclaw' CLI installed on PATH?`));
2153
- child.on('close', async (code) => {
2154
- sendLog(`[zalouser] Login process exited ${code}`);
2155
- if (code === 0 || restartAfterLogin || sent) {
2156
- if (restartAfterLogin) {
2157
- sendLog('[zalouser] Login saved. Restarting native gateway so Zalo User can receive messages...');
2158
- try {
2159
- await run('openclaw', ['gateway', 'stop'], { cwd: projectDir, env: runtimeEnv });
2160
- state.botPid = startDetached('openclaw', ['gateway', 'run'], { cwd: projectDir, env: runtimeEnv });
2161
- sendLog(`[zalouser] Native gateway restarted (pid=${state.botPid || 'unknown'}). Try sending a Zalo message now.`);
2162
- } catch (err) {
2163
- sendLog(`[zalouser] Gateway restart failed: ${err.message}. Restart it manually: openclaw gateway run`);
2164
- }
2165
- }
2166
- zaloLoginInFlight = false;
2167
- } else if (loginAttempt < MAX_LOGIN_ATTEMPTS && !sent) {
2168
- const delay = RETRY_DELAYS[loginAttempt] || 10000;
2169
- sendLog(`[zalouser] QR not ready yet. Waiting ${delay / 1000}s before retry...`);
2170
- setTimeout(runLoginAttempt, delay);
2171
- } else {
2172
- sendLog('[zalouser] All login attempts exhausted. Try clicking "Đăng nhập Zalo" again.');
2173
- zaloLoginInFlight = false;
2174
- }
2175
- });
2176
- };
2177
-
2178
- runLoginAttempt();
2179
- return { message: 'Generating Zalo QR (native). The image will appear automatically.' };
1932
+ throw httpError(400, 'Zalo login cần project Docker đang chạy (không tìm thấy docker-compose.yml).');
2180
1933
  }
2181
1934
 
2182
1935
  function getBotServiceName(projectDir) {
@@ -2392,16 +2145,7 @@ async function updateRuntime(target, projectDir) {
2392
2145
  probeCacheClear();
2393
2146
  return { ok: true, target, spec, mode: 'docker' };
2394
2147
  }
2395
- await run('npm', ['install', '-g', spec]);
2396
- if (isRouter) {
2397
- await run('openclaw', ['gateway', 'stop'], { cwd: projectDir }).catch(() => {});
2398
- await run('npm', ['install', '-g', NINE_ROUTER_NPM_SPEC]);
2399
- } else {
2400
- await run('npm', ['install', '-g', OPENCLAW_NPM_SPEC]);
2401
- }
2402
- await syncRuntimeState(projectDir, { full: true }).catch(() => {});
2403
- probeCacheClear();
2404
- return { ok: true, target, spec, mode: 'native' };
2148
+ throw httpError(400, 'Không có project Docker để cập nhật runtime.');
2405
2149
  }
2406
2150
 
2407
2151
  async function restartDockerBotContainer(projectDir = state.projectDir) {
@@ -2604,6 +2348,192 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
2604
2348
  }
2605
2349
  }
2606
2350
 
2351
+ // Locate a real Chrome/Chromium binary on the host (for the "grant Chrome to the bot" button).
2352
+ async function findChromeBinary() {
2353
+ if (process.platform === 'darwin') {
2354
+ for (const p of [
2355
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
2356
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
2357
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
2358
+ ]) if (existsSync(p)) return p;
2359
+ return '';
2360
+ }
2361
+ if (process.platform === 'win32') {
2362
+ for (const p of [
2363
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
2364
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
2365
+ join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
2366
+ ]) if (p && existsSync(p)) return p;
2367
+ return '';
2368
+ }
2369
+ for (const c of ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium']) {
2370
+ const r = await commandExists(c, ['--version']);
2371
+ if (r.ok) return c;
2372
+ }
2373
+ return '';
2374
+ }
2375
+
2376
+ // TCP relay for headless VPS: `ssh -R 9222:...` binds the VPS loopback only (sshd GatewayPorts
2377
+ // defaults to "no"), which the bot container cannot reach. This relay listens on the docker
2378
+ // bridge IP (host.docker.internal from inside the container) and pipes to the loopback tunnel.
2379
+ let _chromeRelayServer = null;
2380
+ async function getDockerBridgeIp() {
2381
+ try {
2382
+ const out = await runCapture('sh', ['-c', "ip -4 -o addr show docker0 | awk '{print $4}' | cut -d/ -f1"], { shell: false, timeout: 4000 });
2383
+ const ip = String(out.stdout || '').trim();
2384
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(ip)) return ip;
2385
+ } catch {}
2386
+ return '172.17.0.1';
2387
+ }
2388
+ async function ensureChromeRelay() {
2389
+ if (_chromeRelayServer) return true;
2390
+ const bridgeIp = await getDockerBridgeIp();
2391
+ return new Promise((resolveP) => {
2392
+ const relay = net.createServer((client) => {
2393
+ const upstream = net.connect(9222, '127.0.0.1');
2394
+ client.pipe(upstream).pipe(client);
2395
+ client.on('error', () => upstream.destroy());
2396
+ upstream.on('error', () => client.destroy());
2397
+ });
2398
+ relay.once('error', () => resolveP(false)); // EADDRINUSE etc. → likely already relayed
2399
+ relay.listen(9222, bridgeIp, () => {
2400
+ _chromeRelayServer = relay;
2401
+ sendLog(`[chrome] Relay ${bridgeIp}:9222 → 127.0.0.1:9222 sẵn sàng (chờ SSH tunnel từ máy bạn).`);
2402
+ // Ubuntu VPSes usually run ufw with default-deny INPUT, which silently drops container→host
2403
+ // traffic to the relay. Open the port scoped to the PRIVATE bridge IP only (not reachable
2404
+ // from the internet). Best-effort; `ufw allow` skips duplicates on re-runs.
2405
+ run('sh', ['-c', `command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active" && ufw allow in to ${bridgeIp} port 9222 proto tcp comment "openclaw chrome-debug relay (docker bridge only)" || true`])
2406
+ .catch(() => sendLog('[chrome] Không thể tự mở firewall cho relay — nếu bot không thấy Chrome, chạy: ufw allow in to ' + bridgeIp + ' port 9222 proto tcp'));
2407
+ resolveP(true);
2408
+ });
2409
+ });
2410
+ }
2411
+
2412
+ // Launch real host Chrome in remote-debugging mode (port 9222) so the browser-automation plugin
2413
+ // can drive the user's actual Chrome (logged-in profile) instead of headless Chromium. The bot
2414
+ // reaches it via CDP (host.docker.internal:9222 from the container). Detached: keeps running after
2415
+ // this request. `--remote-allow-origins=*` is required by modern Chrome for cross-origin CDP.
2416
+ // On a headless VPS there is no Chrome to open here — instead we start the bridge relay and hand
2417
+ // back copy-paste commands so the user runs Chrome on THEIR machine + a reverse SSH tunnel.
2418
+ async function startChromeDebug() {
2419
+ if (isHeadlessServer()) {
2420
+ await ensureChromeRelay();
2421
+ const ip = (await getPublicIp().catch(() => '')) || '<IP-VPS>';
2422
+ const user = sshUserName();
2423
+ return {
2424
+ ok: true,
2425
+ headless: true,
2426
+ port: 9222,
2427
+ chromeCmdMac: `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222 --user-data-dir="$HOME/.openclaw-chrome-debug" --remote-allow-origins='*'`,
2428
+ chromeCmdWin: `"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222 --user-data-dir=%TEMP%\\openclaw-chrome-debug --remote-allow-origins=*`,
2429
+ tunnelCmd: `ssh -N -R 9222:127.0.0.1:9222 ${user}@${ip}`,
2430
+ };
2431
+ }
2432
+ const bin = await findChromeBinary();
2433
+ if (!bin) {
2434
+ throw httpError(400, process.platform === 'linux'
2435
+ ? 'Không tìm thấy Chrome/Chromium trên máy. Cài google-chrome hoặc chromium rồi thử lại (không áp dụng cho VPS không có giao diện).'
2436
+ : 'Không tìm thấy Google Chrome. Hãy cài Chrome rồi thử lại.');
2437
+ }
2438
+ const port = 9222;
2439
+ const userDataDir = join(os.tmpdir(), 'openclaw-chrome-debug');
2440
+ const args = [
2441
+ `--remote-debugging-port=${port}`,
2442
+ `--user-data-dir=${userDataDir}`,
2443
+ '--remote-allow-origins=*',
2444
+ '--no-first-run',
2445
+ '--no-default-browser-check',
2446
+ ];
2447
+ const child = spawn(bin, args, { detached: true, stdio: 'ignore', windowsHide: false });
2448
+ child.on('error', (e) => sendLog(`[chrome] Không mở được Chrome debug: ${e.message}`));
2449
+ child.unref();
2450
+ sendLog(`[chrome] Đã mở Chrome debug ở cổng ${port} (${bin}). Bot sẽ ưu tiên dùng Chrome này.`);
2451
+ return { ok: true, port, browser: bin, userDataDir };
2452
+ }
2453
+
2454
+ // Poll until the Docker daemon responds (or timeout). Returns true if ready.
2455
+ async function waitForDockerDaemon(timeoutMs) {
2456
+ const deadline = Date.now() + timeoutMs;
2457
+ for (;;) {
2458
+ const ok = await commandExists('docker', ['version', '--format', '{{.Server.Version}}']);
2459
+ if (ok.ok) return true;
2460
+ if (Date.now() >= deadline) return false;
2461
+ await new Promise((r) => setTimeout(r, 3000));
2462
+ }
2463
+ }
2464
+
2465
+ // Ensure Docker (engine + compose) is available before a docker-mode install, auto-installing the
2466
+ // latest version appropriate for the host OS when it is missing:
2467
+ // • Linux → Docker's official convenience script (get.docker.com; auto-detects the distro) + start daemon
2468
+ // • macOS → Docker Desktop via Homebrew cask, then launch it
2469
+ // • Windows → Docker Desktop via winget (fallback Chocolatey), then launch it
2470
+ // macOS/Windows Docker Desktop needs a GUI/WSL startup (and sometimes a reboot), so we install +
2471
+ // launch + wait, and give a clear next-step if the daemon still isn't up when we time out.
2472
+ async function ensureDockerInstalled(osChoice) {
2473
+ if (await waitForDockerDaemon(0)) return; // already running
2474
+ const cliOk = await commandExists('docker', ['--version']);
2475
+
2476
+ if (process.platform === 'linux') {
2477
+ const root = typeof process.getuid === 'function' && process.getuid() === 0;
2478
+ const sudo = root ? '' : 'sudo ';
2479
+ if (!cliOk.ok) {
2480
+ sendLog('[docker] Chưa có Docker — đang tự cài Docker Engine mới nhất qua script chính thức get.docker.com (1–3 phút)...');
2481
+ await run('sh', ['-c', `curl -fsSL https://get.docker.com -o /tmp/get-docker.sh && ${sudo}sh /tmp/get-docker.sh`]);
2482
+ if (!root) await run('sh', ['-c', 'sudo usermod -aG docker "$USER" || true']).catch(() => {});
2483
+ }
2484
+ sendLog('[docker] Bật & khởi động Docker daemon...');
2485
+ await run('sh', ['-c', `${sudo}systemctl enable --now docker`]).catch(() => {});
2486
+ if (!(await waitForDockerDaemon(20000))) {
2487
+ throw httpError(500, 'Đã cài Docker nhưng daemon chưa chạy. Hãy chạy `systemctl start docker` (hoặc đăng nhập lại nếu vừa thêm vào nhóm docker) rồi cài lại.');
2488
+ }
2489
+ sendLog('[docker] Docker đã sẵn sàng.');
2490
+ return;
2491
+ }
2492
+
2493
+ if (process.platform === 'darwin') {
2494
+ if (!cliOk.ok) {
2495
+ const brew = await commandExists('brew', ['--version']);
2496
+ if (!brew.ok) {
2497
+ throw httpError(400, 'macOS: cần Homebrew để tự cài Docker Desktop. Cài Homebrew tại https://brew.sh (hoặc cài Docker Desktop thủ công) rồi cài lại.');
2498
+ }
2499
+ sendLog('[docker] macOS: đang cài Docker Desktop mới nhất qua Homebrew (brew install --cask docker)...');
2500
+ await run('brew', ['install', '--cask', 'docker']);
2501
+ }
2502
+ sendLog('[docker] Mở Docker Desktop và chờ daemon khởi động...');
2503
+ await run('open', ['-a', 'Docker']).catch(() => {});
2504
+ if (!(await waitForDockerDaemon(120000))) {
2505
+ throw httpError(500, 'Đã cài Docker Desktop — hãy mở Docker Desktop, hoàn tất cấp quyền lần đầu, đợi biểu tượng cá voi báo "running" rồi cài lại.');
2506
+ }
2507
+ sendLog('[docker] Docker đã sẵn sàng.');
2508
+ return;
2509
+ }
2510
+
2511
+ if (process.platform === 'win32') {
2512
+ if (!cliOk.ok) {
2513
+ const winget = await commandExists('winget', ['--version']);
2514
+ const choco = await commandExists('choco', ['--version']);
2515
+ if (winget.ok) {
2516
+ sendLog('[docker] Windows: đang cài Docker Desktop mới nhất qua winget...');
2517
+ await run('winget', ['install', '-e', '--id', 'Docker.DockerDesktop', '--accept-source-agreements', '--accept-package-agreements']);
2518
+ } else if (choco.ok) {
2519
+ sendLog('[docker] Windows: đang cài Docker Desktop qua Chocolatey...');
2520
+ await run('choco', ['install', 'docker-desktop', '-y']);
2521
+ } else {
2522
+ throw httpError(400, 'Windows: cần winget hoặc Chocolatey để tự cài Docker Desktop (hoặc cài thủ công tại https://www.docker.com). Cài xong rồi thử lại.');
2523
+ }
2524
+ }
2525
+ sendLog('[docker] Mở Docker Desktop và chờ daemon khởi động...');
2526
+ await run('cmd', ['/c', 'start', '', '%ProgramFiles%\\Docker\\Docker\\Docker Desktop.exe']).catch(() => {});
2527
+ if (!(await waitForDockerDaemon(120000))) {
2528
+ throw httpError(500, 'Đã cài Docker Desktop — Windows có thể cần bật WSL2 và khởi động lại máy. Hãy mở Docker Desktop, đợi "running" (hoặc reboot nếu được yêu cầu) rồi cài lại.');
2529
+ }
2530
+ sendLog('[docker] Docker đã sẵn sàng.');
2531
+ return;
2532
+ }
2533
+
2534
+ throw httpError(400, 'Hệ điều hành không được hỗ trợ tự cài Docker. Hãy cài Docker thủ công rồi thử lại.');
2535
+ }
2536
+
2607
2537
  async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, routerPort = 20128 }) {
2608
2538
  state.installing = true;
2609
2539
  state.installed = false;
@@ -2615,6 +2545,9 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
2615
2545
  try {
2616
2546
  sendLog('OpenClaw local installer started');
2617
2547
  sendLog(`Target: OS=${osChoice}, mode=${mode}, project=${projectDir}, gatewayPort=${gatewayPort}, routerPort=${routerPort}`);
2548
+ // Make sure Docker is present (auto-install on Linux/VPS) before doing any work — fail fast
2549
+ // with a clear message rather than deep inside `docker compose up`.
2550
+ await ensureDockerInstalled(osChoice);
2618
2551
  await writeCoreProject({ projectDir, osChoice, mode, gatewayPort, routerPort });
2619
2552
  await run('npm', ['install', '-g', OPENCLAW_NPM_SPEC]);
2620
2553
  await run('npm', ['install', '-g', NINE_ROUTER_NPM_SPEC]);
@@ -2631,63 +2564,6 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
2631
2564
  await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
2632
2565
  await applyResolved9RouterApiKey(projectDir).catch(() => {});
2633
2566
  await recreateDockerBot(projectDir).catch(() => {});
2634
- } else {
2635
- const nineRouterDataDir = join(projectDir, '.9router');
2636
- const nineRouterDbPath = join(nineRouterDataDir, 'db', 'data.sqlite');
2637
- const runtimeEnv = {
2638
- OPENCLAW_HOME: join(projectDir, '.openclaw'),
2639
- OPENCLAW_STATE_DIR: join(projectDir, '.openclaw'),
2640
- DATA_DIR: nineRouterDataDir,
2641
- OPENCLAW_GATEWAY_PORT: String(gatewayPort),
2642
- OPENCLAW_PORT: String(gatewayPort),
2643
- // Parity with the docker runtime env (docker-gen.js) so the gateway and browser plugin
2644
- // behave the same natively: allow the private-network control-UI websocket and tell the
2645
- // browser plugin which host OS it is running on.
2646
- OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: '1',
2647
- OPENCLAW_SETUP_OS: osChoice || '',
2648
- OPENCLAW_BROWSER_HOST_OS: osChoice || '',
2649
- };
2650
- await run('openclaw', ['gateway', 'stop'], { cwd: projectDir, env: runtimeEnv }).catch(() => {});
2651
- // 9router binds loopback in native mode on purpose: openclaw runs on the SAME host and
2652
- // reaches it via localhost (see get9RouterBaseUrl), so there is no reason to expose the LLM
2653
- // proxy on 0.0.0.0 — even on a VPS that would be an open relay. Only the gateway is exposed.
2654
- // Start 9router first (auto-restart supervised), then sync, then the gateway — so the gateway
2655
- // boots after 9router is reachable and after the API key has been written to its config.
2656
- const routerMethod = await startNativeService({
2657
- projectDir, name: '9router', cmd: '9router', desc: `OpenClaw 9router (${nativeServiceId(projectDir)})`,
2658
- args: ['-n', '-l', '-H', '127.0.0.1', '-p', String(routerPort), '--skip-update'], env: runtimeEnv,
2659
- });
2660
- // Smart-route auto-sync: the docker sidecar runs a script that disables 9router's login
2661
- // requirement and builds the default `smart-route` combo from the active providers. Native
2662
- // mode skipped this, so the default model had no backing models. Reuse the exact docker
2663
- // script with the native DB path (passed via env to stay cross-platform / shell-safe).
2664
- try {
2665
- const artifacts = buildDockerArtifacts({ is9Router: true, osChoice, openClawNpmSpec: OPENCLAW_NPM_SPEC, gatewayPort, routerPort });
2666
- if (artifacts.syncScript) {
2667
- await fsp.mkdir(nineRouterDataDir, { recursive: true });
2668
- const syncPath = join(nineRouterDataDir, 'sync.js');
2669
- await fsp.writeFile(syncPath, artifacts.syncScript, 'utf8');
2670
- // Run the sync under supervision too (managed service) so it is torn down cleanly on
2671
- // delete instead of leaking an orphaned `node sync.js` process.
2672
- await startNativeService({
2673
- projectDir, name: 'sync', cmd: process.execPath, args: [syncPath],
2674
- desc: `OpenClaw 9router smart-route sync (${nativeServiceId(projectDir)})`,
2675
- env: { ...runtimeEnv, NINEROUTER_DB_PATH: nineRouterDbPath, PORT: String(routerPort) },
2676
- });
2677
- sendLog('[native] 9router smart-route sync started');
2678
- }
2679
- } catch (e) {
2680
- sendLog(`[native] smart-route sync setup skipped: ${e.message}`);
2681
- }
2682
- // Give 9router a moment to boot + sync to disable its login gate, then resolve its API key
2683
- // into openclaw.json BEFORE the gateway starts (so the gateway reads it on first boot).
2684
- await new Promise((r) => setTimeout(r, 8000));
2685
- await applyResolved9RouterApiKey(projectDir).catch(() => {});
2686
- const gatewayMethod = await startNativeService({
2687
- projectDir, name: 'gateway', cmd: 'openclaw', desc: `OpenClaw gateway (${nativeServiceId(projectDir)})`,
2688
- args: ['gateway', 'run'], env: runtimeEnv,
2689
- });
2690
- sendLog(`Native runtime started — 9router via ${routerMethod}, gateway via ${gatewayMethod}${state.botPid ? ` (pid=${state.botPid})` : ''}`);
2691
2567
  }
2692
2568
  state.installed = true;
2693
2569
  sendLog('✅ Install completed');
@@ -3092,10 +2968,6 @@ async function deleteProjectFolder(projectDir, rootProjectDir) {
3092
2968
  });
3093
2969
  // Sleep 2.5 seconds to let Windows file system release overlays/locks
3094
2970
  await new Promise((resolve) => setTimeout(resolve, 2500));
3095
- } else {
3096
- // Native project: stop & remove any launchd/systemd auto-restart services so they don't keep
3097
- // respawning a deleted bot. No-op when no services were registered.
3098
- await removeNativeAutostart(resolved);
3099
2971
  }
3100
2972
 
3101
2973
  try {
@@ -3984,6 +3856,9 @@ async function handler(req, res, rootProjectDir) {
3984
3856
  const projectDir = await resolveProjectDir(rootProjectDir, body);
3985
3857
  return json(res, await addBotMount(projectDir, body.hostPath, body.mountName));
3986
3858
  }
3859
+ if (url.pathname === '/api/browser/start-chrome-debug' && req.method === 'POST') {
3860
+ return json(res, await startChromeDebug());
3861
+ }
3987
3862
  if (url.pathname === '/api/setup/update' && req.method === 'POST') {
3988
3863
  const installerDir = resolve(__dirname, '../..');
3989
3864
  const isGit = existsSync(resolve(installerDir, '.git'));
@@ -4100,11 +3975,19 @@ async function handler(req, res, rootProjectDir) {
4100
3975
  return json(res, { name, content: await fsp.readFile(file, 'utf8') });
4101
3976
  }
4102
3977
  if (req.method === 'PUT') {
4103
- if (!name.endsWith('.md')) throw httpError(400, 'Only markdown files (.md) can be modified');
3978
+ // Allow the same text types the file tree marks editable (it exposes .json/.js/.yml/…,
3979
+ // not just .md — the old .md-only guard made "Save" silently fail on those files).
3980
+ const writableExt = new Set(['.md', '.txt', '.json', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.yml', '.yaml', '.env', '.sh', '.bat', '.ps1', '.html', '.css']);
3981
+ if (!writableExt.has(extname(name).toLowerCase())) throw httpError(400, `Loại file này không hỗ trợ sửa từ UI (${extname(name) || 'không có đuôi'})`);
4104
3982
  const body = await readJson(req);
4105
3983
  const projectDir = await resolveProjectDir(rootProjectDir, body);
4106
3984
  const file = safeJoin(projectDir, name);
4107
- await fsp.writeFile(file, String(body.content || ''), 'utf8');
3985
+ const content = String(body.content || '');
3986
+ // Don't let a typo brick openclaw.json & friends — reject invalid JSON with a clear error.
3987
+ if (extname(name).toLowerCase() === '.json') {
3988
+ try { JSON.parse(content); } catch (e) { throw httpError(400, `JSON không hợp lệ: ${e.message}`); }
3989
+ }
3990
+ await fsp.writeFile(file, content, 'utf8');
4108
3991
  return json(res, { ok: true });
4109
3992
  }
4110
3993
  }
@@ -4318,7 +4201,7 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
4318
4201
  printRemoteAccessHint(port).catch(() => {});
4319
4202
  }
4320
4203
 
4321
- export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloUserLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, removeNativeAutostart };
4204
+ export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloUserLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder };
4322
4205
 
4323
4206
 
4324
4207
 
package/dist/web/app.js CHANGED
@@ -7,9 +7,9 @@ const OS_OPTIONS = [
7
7
  { id: 'linux-desktop', title: 'Linux Desktop', subtitle: 'Ubuntu / Debian / Fedora', icon: `${SVG_CDN}/linux/default.svg`, badge: 'Desktop' },
8
8
  { id: 'vps', title: 'Linux VPS', subtitle: 'Server install with public bind', icon: `${SVG_CDN}/ubuntu/default.svg`, badge: 'Server' },
9
9
  ];
10
+ // Docker is the only supported deploy mode. (Native mode was removed.)
10
11
  const MODE_OPTIONS = [
11
12
  { id: 'docker', title: 'Docker', subtitle: 'Isolated containers, safest default', icon: `${SVG_CDN}/docker/default.svg`, badge: 'Recommended' },
12
- { id: 'native', title: 'Native', subtitle: 'Direct host install, lighter runtime', icon: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%231b1113'/%3E%3Cpath d='M15 22l9 10-9 10' fill='none' stroke='%23ff3b4d' stroke-width='5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M30 42h19' fill='none' stroke='%23f8f7f7' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E`, badge: 'Advanced' },
13
13
  ];
14
14
  const BOT_CHANNELS = [
15
15
  { id: 'telegram', title: 'Telegram', subtitle: 'Bot API', icon: `${SVG_CDN}/telegram/default.svg`, badge: 'Tele' },
@@ -155,7 +155,7 @@ function fbPluginModal() {
155
155
  return `<div class="modal-backdrop confirm-backdrop" data-fbplugin="close">
156
156
  <section class="donate-modal confirm-modal" role="dialog" aria-modal="true" aria-label="fb-messenger plugin" onclick="event.stopPropagation()">
157
157
  <button class="modal-x" data-fbplugin="close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
158
- <div class="donate-head"><span aria-hidden="true">🔌</span><div><p>${t('Cần plugin','Plugin required')}</p><h2>fb-messenger</h2><small>${t('Để tạo bot <b>Facebook Messenger</b>, bạn cần plugin <b>fb-messenger</b> do <b>tuanminhhole</b> cung cấp. Plugin hiện chưa public vui lòng liên hệ để nhận.','To create a <b>Facebook Messenger</b> bot you need the <b>fb-messenger</b> plugin provided by <b>tuanminhhole</b>. It is not public yet please reach out to get it.')}</small></div></div>
158
+ <div class="donate-head"><span aria-hidden="true">🔌</span><div><p>${t('Cần plugin','Plugin required')}</p><h2>fb-messenger</h2><small>${t('Bot <b>Facebook Messenger</b> cần plugin <b>fb-messenger</b> (đã public trên ClawHub). Bấm <b>Tạo bot</b>, rồi mở <b>Bot Plugins</b> bấm <b>Cài</b> thẻ <b>openclaw-fb-messenger</b>.','A <b>Facebook Messenger</b> bot needs the <b>fb-messenger</b> plugin (now public on ClawHub). Click <b>Create bot</b>, then open <b>Bot Plugins</b> and hit <b>Install</b> on the <b>openclaw-fb-messenger</b> card.')}</small></div></div>
159
159
  <div class="socials" style="justify-content:center;display:flex;gap:10px;width:100%;margin:8px 0 4px;">
160
160
  ${socials.map(([n,u])=>`<a href="${u}" target="_blank" rel="noopener" aria-label="${n}">${socialIcon(n)}</a>`).join('')}
161
161
  </div>
@@ -169,8 +169,9 @@ function confirmModal() {
169
169
  return `<div class="modal-backdrop confirm-backdrop" data-confirm-action="cancel">
170
170
  <section class="donate-modal confirm-modal" role="dialog" aria-modal="true" aria-label="${escapeHtml(m.title)}" onclick="event.stopPropagation()">
171
171
  <button class="modal-x" data-confirm-action="cancel" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
172
- <div class="donate-head"><span aria-hidden="true">⚠</span><div><p>${escapeHtml(m.eyebrow || t('Xác nhận','Confirm'))}</p><h2>${escapeHtml(m.title)}</h2><small>${escapeHtml(m.message || '')}</small></div></div>
173
- <div class="confirm-actions"><button class="secondary" data-confirm-action="cancel">${t('Hủy','Cancel')}</button><button class="primary danger" data-confirm-action="ok">${escapeHtml(m.okText || t('Xóa','Delete'))}</button></div>
172
+ <div class="donate-head"><span aria-hidden="true">${escapeHtml(m.icon || '⚠')}</span><div><p>${escapeHtml(m.eyebrow || t('Xác nhận','Confirm'))}</p><h2>${escapeHtml(m.title)}</h2><small>${escapeHtml(m.message || '')}</small></div></div>
173
+ ${m.bodyHtml ? `<div class="confirm-body">${m.bodyHtml}</div>` : ''}
174
+ <div class="confirm-actions">${m.hideCancel ? '' : `<button class="secondary" data-confirm-action="cancel">${t('Hủy','Cancel')}</button>`}<button class="primary ${m.okDanger === false ? '' : 'danger'}" data-confirm-action="ok">${escapeHtml(m.okText || t('Xóa','Delete'))}</button></div>
174
175
  </section>
175
176
  </div>`;
176
177
  }
@@ -210,7 +211,6 @@ function installModal() {
210
211
  const osChoices = OS_OPTIONS.map(o => [o.id, t(o.title, o.title), trChoice(o).subtitle]);
211
212
  const modeChoices = [
212
213
  ['docker', 'Docker', t('\u0043ontainer c\u00f4 l\u1eadp, an to\u00e0n nh\u1ea5t', 'Isolated containers, safest default')],
213
- ['native', 'Native', t('C\u00e0i tr\u1ef1c ti\u1ebfp, runtime nh\u1eb9 h\u01a1n', 'Direct host install, lighter runtime')],
214
214
  ];
215
215
  return `<div class="modal-backdrop install-backdrop" data-install-modal="close">
216
216
  <section class="donate-modal install-modal" role="dialog" aria-modal="true" aria-label="${t('T\u1ea1o Project','Create Project')}" onclick="event.stopPropagation()">
@@ -519,7 +519,7 @@ function renderFilesPanel() {
519
519
  // Re-wire only file-panel-specific handlers
520
520
  panel.querySelectorAll('[data-select-file]').forEach(btn => btn.onclick = () => { state.selectedFile = btn.dataset.selectFile; renderFilesPanel(); });
521
521
  panel.querySelectorAll('[data-toggle-dir]').forEach(btn => btn.onclick = () => { const p = btn.dataset.toggleDir; state.openDirs[p] = !(state.openDirs[p] ?? true); renderFilesPanel(); });
522
- panel.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { projectDir: activeProjectDir(), content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
522
+ panel.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; try { await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { projectDir: activeProjectDir(), content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); } catch (err) { showToast(t('Lưu thất bại', 'Save failed'), err.message, 'error'); } }));
523
523
  }
524
524
 
525
525
  function renderSkillsPanel() {
@@ -564,6 +564,34 @@ function wireSkillsHandlers(scope = document) {
564
564
  }
565
565
  renderSkillsPanel();
566
566
  });
567
+ scope.querySelectorAll('[data-chrome-debug]').forEach(btn => btn.onclick = async () => {
568
+ btn.disabled = true;
569
+ try {
570
+ const r = await api('/api/browser/start-chrome-debug', { method: 'POST', body: {} });
571
+ if (r.headless) {
572
+ // VPS has no display — guide the user to run Chrome on THEIR machine + a reverse tunnel.
573
+ state.confirmModal = {
574
+ icon: '🌐',
575
+ eyebrow: t('Chrome debug', 'Chrome debug'),
576
+ title: t('Dùng Chrome trên máy của bạn', 'Use Chrome on your computer'),
577
+ message: t('VPS không có màn hình nên không mở Chrome tại đây. Chạy 2 lệnh dưới trên máy của bạn và GIỮ chúng chạy — bot sẽ tự dùng Chrome đó (cổng 9222). Không làm cũng không sao: bot vẫn có trình duyệt ẩn (headless) sẵn trong container.', 'This VPS has no display. Run the two commands below on YOUR computer and keep them running — the bot will use that Chrome (port 9222). Optional: the bot already has a built-in headless browser.'),
578
+ bodyHtml: `<p class="guide-step">1. ${t('Mở Chrome debug trên máy bạn — macOS:', 'Start Chrome debug on your machine — macOS:')}</p><code class="cmd">${escapeHtml(r.chromeCmdMac)}</code><p class="guide-step">${t('Hoặc Windows:', 'Or Windows:')}</p><code class="cmd">${escapeHtml(r.chromeCmdWin)}</code><p class="guide-step">2. ${t('Tạo tunnel về VPS (cửa sổ terminal khác):', 'Create the reverse tunnel (another terminal):')}</p><code class="cmd">${escapeHtml(r.tunnelCmd)}</code>`,
579
+ okText: t('Đã hiểu', 'Got it'),
580
+ okDanger: false,
581
+ hideCancel: true,
582
+ onConfirm: () => { state.confirmModal = null; render(); },
583
+ };
584
+ render();
585
+ } else {
586
+ showToast(t('Đã mở Chrome debug', 'Chrome debug started'),
587
+ `${t('Cổng', 'Port')} ${r.port} — ${t('bot sẽ ưu tiên dùng Chrome này', 'the bot will prefer this Chrome')}`, 'success');
588
+ }
589
+ } catch (err) {
590
+ showToast(t('Thất bại', 'Failed'), err.message, 'error');
591
+ } finally {
592
+ btn.disabled = false;
593
+ }
594
+ });
567
595
  }
568
596
 
569
597
  function title() {
@@ -907,11 +935,17 @@ function joinDisplayPath(root = '', child = '') {
907
935
  const r = String(root || '').replace(/[\\/]+$/, '');
908
936
  const c = String(child || '').replace(/^[\\/]+/, '');
909
937
  if (!r) return c || '-';
910
- return c ? `${r}\\${c.replace(/\//g, '\\')}` : r;
938
+ if (!c) return r;
939
+ // Match the separator style of the project root: Windows paths (C:\...) join with "\",
940
+ // POSIX paths join with "/" (the old always-backslash join rendered Linux paths mangled).
941
+ const win = /^[A-Za-z]:/.test(r) || r.includes('\\');
942
+ return win ? `${r}\\${c.replace(/\//g, '\\')}` : `${r}/${c.replace(/\\/g, '/')}`;
911
943
  }
912
944
  function projectPathLine(bot = currentBot(), fileName = '') {
913
945
  const s = state.install || {};
914
- const workspace = bot?.workspace || '';
946
+ // Legacy configs stored the container-absolute workspace; show it relative to the project root
947
+ // (new configs are already relative — see bot-config-gen).
948
+ const workspace = String(bot?.workspace || '').replace(/^\/home\/node\/project\/?/, '');
915
949
  const relFile = fileName ? String(fileName).replace(/^\.openclaw[\\/][^\\/]+[\\/]?/, '') : '';
916
950
  const full = workspace ? joinDisplayPath(s.projectDir || '', relFile ? `${workspace}/${relFile}` : workspace) : (s.projectDir || '-');
917
951
  const label = workspace ? t('Workspace','Workspace') : ui('project');
@@ -972,6 +1006,7 @@ function botSkillsPanel() {
972
1006
  { id: 'memory-tencentdb', title: 'TencentDB Agent Memory', desc: 'Bộ nhớ phân tầng L0–L3 + nén ngữ cảnh: nhớ tốt session dài, tiết kiệm ~61% token (local SQLite, không cần API)' },
973
1007
  { id: 'openclaw-browser-automation', title: 'openclaw-browser-automation', desc: 'Smart Search + Browser (headless & Chrome thật)' },
974
1008
  { id: 'openclaw-zalo-mod', title: 'openclaw-zalo-mod', desc: 'Zalo group helpers', channels: ['zalo-personal'], openWebPort: 18790, openWebPath: '/dashboard' },
1009
+ { id: 'openclaw-fb-messenger', title: 'openclaw-fb-messenger', desc: t('Kênh Facebook Messenger — webhook + Graph API (bắt buộc cho bot Messenger)', 'Facebook Messenger channel — webhook + Graph API (required for Messenger bots)'), channels: ['fb-messenger'] },
975
1010
  { id: 'openclaw-facebook-crawler', title: 'openclaw-facebook-crawler', desc: 'Facebook crawler automation', channels: ['fb-messenger'] },
976
1011
  { id: 'openclaw-n8n-facebook-poster', title: 'openclaw-n8n-facebook-poster', desc: 'Facebook post automation (n8n)', channels: ['fb-messenger'] },
977
1012
  ];
@@ -1003,6 +1038,9 @@ function botSkillsPanel() {
1003
1038
  if (requiresInstall) {
1004
1039
  secs.push(`<button class="secondary icon-btn2 update-plugin-btn" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''} title="${t('Cập nhật lên bản mới nhất','Update to latest version')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px; color:#ffb020; border-color: rgba(255,176,32,0.25); background: rgba(255,176,32,0.05);">${actionIcon('refresh')}<span>${t('Cập nhật','Update')}</span></button>`);
1005
1040
  }
1041
+ if (item.id === 'browser-automation' || item.id === 'openclaw-browser-automation') {
1042
+ secs.push(`<button class="secondary icon-btn2" type="button" data-chrome-debug title="${t('Mở Chrome (chế độ debug) trên máy này để bot dùng trình duyệt thật (đã đăng nhập)','Open real Chrome in debug mode on this machine so the bot can use it')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px; color:#4285F4; border-color: rgba(66,133,244,0.3); background: rgba(66,133,244,0.06);">${actionIcon('link')}<span>${t('Mở Chrome debug','Open Chrome debug')}</span></button>`);
1043
+ }
1006
1044
  } else {
1007
1045
  secs.push(`<button class="secondary icon-btn2" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''}>${actionIcon('download')} ${ui('installVerb')}</button>`);
1008
1046
  }
@@ -1371,13 +1409,9 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1371
1409
  });
1372
1410
  $('#bot-create')?.addEventListener('submit', async (ev) => {
1373
1411
  ev.preventDefault();
1374
- // Facebook Messenger needs the private fb-messenger plugin. If it isn't installed,
1375
- // show a contact modal instead of creating a broken bot.
1376
- if (state.botChannel === 'fb-messenger' && !state.botEditId && !state.featureInstalled?.['plugin:openclaw-fb-messenger']) {
1377
- state.fbPluginModalOpen = true;
1378
- render();
1379
- return;
1380
- }
1412
+ // Facebook Messenger needs the fb-messenger plugin (public on ClawHub). We don't block
1413
+ // creation the heads-up modal on channel-select tells the user to install it from the
1414
+ // bot's Plugins section afterwards (installable via the openclaw-fb-messenger card).
1381
1415
  const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
1382
1416
  if (submitBtn?.classList.contains('is-loading')) return;
1383
1417
  await withButtonLoading(submitBtn, async () => {
@@ -1428,7 +1462,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1428
1462
  }
1429
1463
  });
1430
1464
  });
1431
- document.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { projectDir: activeProjectDir(), content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
1465
+ document.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; try { await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { projectDir: activeProjectDir(), content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); } catch (err) { showToast(t('Lưu thất bại', 'Save failed'), err.message, 'error'); } }));
1432
1466
  wireSkillsHandlers(document);
1433
1467
  }
1434
1468
 
@@ -419,7 +419,6 @@ html[data-theme="light"] .bot-form-grid input,html[data-theme="light"] .bot-form
419
419
  .mini-pill{display:inline-flex;align-items:center;min-height:26px;padding:4px 10px;border-radius:999px;background:rgba(255,255,255,.04);border:1px solid rgba(255,36,54,.55);font-size:12px;font-weight:800;letter-spacing:.2px}
420
420
  .mini-pill--os{border-color:#64748b;color:#cbd5e1}
421
421
  .mini-pill--docker{border-color:#2496ed;color:#9bd5ff}
422
- .mini-pill--native{border-color:#7c5cff;color:#cfc4ff}
423
422
  .channel-tabs{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:18px}
424
423
  .channel-tabs button{display:inline-flex;align-items:center;gap:8px;border:1px solid rgba(255,36,54,.24);background:rgba(255,255,255,.045);color:var(--body);border-radius:999px;padding:9px 12px;font-weight:900;cursor:pointer}
425
424
  .channel-tabs button.active{background:rgba(255,36,54,.16);border-color:var(--primary);color:var(--ink);box-shadow:0 0 0 1px rgba(255,36,54,.18) inset}
@@ -1931,6 +1930,12 @@ body:has(.modal-backdrop) .bottom{display:none!important}
1931
1930
  /* Bot / Cài đặt hero buttons sit side-by-side — keep their labels on one line. */
1932
1931
  .dash-actions .icon-btn2{white-space:nowrap}
1933
1932
 
1933
+ /* [Confirm modal] Optional body block for step-by-step guides (e.g. Chrome-debug on a VPS):
1934
+ copyable command lines with wrap, compact spacing. */
1935
+ .confirm-body{display:grid;gap:8px;margin:14px 0 2px}
1936
+ .confirm-body .cmd{margin-top:0;user-select:all;word-break:break-all;white-space:pre-wrap;font-size:11.5px;line-height:1.5}
1937
+ .guide-step{margin:4px 0 0;color:var(--body);font-size:13px;font-weight:700}
1938
+
1934
1939
  @media(max-width:760px){
1935
1940
  /* [Topbar] Keep search + theme + language on ONE row; theme becomes icon-only and the
1936
1941
  language globe icon is dropped so the whole bar stays compact. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.10.1",
3
+ "version": "5.11.0",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {