create-openclaw-bot 5.10.0 → 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.0-0EA5E9?style=for-the-badge" alt="Version 5.10.0" /></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,26 @@
27
27
 
28
28
  ---
29
29
 
30
- ## 🆕 What's New in v5.10.0
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>
38
+
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.
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.
41
+ - 🔐 **9router first-install sync — fixed**: no longer permanently disables "Require login". The generated `sync.js` logs in with 9router's default password `123456`, builds the `smart-route` combo from active providers **once**, then stops. Require login stays ON (change the password later); `/v1` model calls are unaffected.
42
+ - 📁 **Native workspace path — fixed**: agent `workspace` is now relative, so persona/memory/skills resolve correctly on native installs (previously a container-absolute path pointing nowhere on the host).
43
+ - 🧩 **Config `meta` no longer clobbered**: the generator stops seeding `meta`, so OpenClaw owns it — prevents config parse failures from a version-range `lastTouchedVersion`.
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.
45
+
46
+ </details>
47
+
48
+ <details>
49
+ <summary><b>Previous: What's new in v5.9.0</b></summary>
31
50
 
32
51
  - 🧠 **TencentDB Agent Memory — 1-click install**: New memory plugin in the UI. A 4-tier (L0–L3) layered-memory + context-compression engine that keeps long sessions coherent and **cuts token usage by up to ~61%**. Fully local (SQLite), no API key, Docker-ready.
33
52
  - ⚡ **Token-lean defaults for every new bot**: Ships with context pruning (cache-TTL) + `safeguard` compaction — **cheaper & sharper long conversations** with zero tuning.
@@ -36,6 +55,8 @@
36
55
  - 🐳 **One-click Docker controls**: Restart / Rebuild the bot container and grant it disk access (mount any host folder at `/mnt/<name>`, cross-OS) right from the dashboard — restart your bot without touching the command line.
37
56
  - ⚡ **Faster Dashboard & Bot pages**: Runtime/version detection now probes **once** and is cached instead of re-running on every page load — bot status dropped from ~4s to ~3ms on repeat loads. Cache auto-invalidates on update/rebuild/restart/install.
38
57
 
58
+ </details>
59
+
39
60
  <details>
40
61
  <summary><b>Previous: What's new in v5.8.24 (Launcher Auto-Update)</b></summary>
41
62
 
@@ -190,7 +211,7 @@ npx github:tuanminhhole/openclaw-setup
190
211
  - **Telegram**: Acquire your official Bot Token from `@BotFather`.
191
212
  - **Zalo Bot API**: Obtain credentials from [developers.zalo.me](https://developers.zalo.me).
192
213
  - **Zalo Personal**: Scan the QR authorization image displayed on the OpenClaw Dashboard.
193
- - **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.
194
215
  - **Discord**: _Coming soon._
195
216
  - **Lark**: _Coming soon._
196
217
 
@@ -275,7 +296,7 @@ Companion repos to build a complete, self-running AI assistant:
275
296
  - [vietbrain](https://github.com/tuanminhhole/vietbrain) — Vietnamese "Second Brain" framework for Obsidian (AI-ready)
276
297
 
277
298
  **🔌 Plugins (runtime)**
278
- - `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
279
300
  - [openclaw-telegram-multibot-relay](https://github.com/tuanminhhole/openclaw-telegram-multibot-relay) — Multibot Telegram relay, delegation & native cron reminders
280
301
  - [openclaw-zalo-mod](https://github.com/tuanminhhole/openclaw-zalo-mod) — Zero-token Zalo group management (slash commands, anti-spam, warn, memory)
281
302
  - [openclaw-browser-automation](https://github.com/tuanminhhole/openclaw-browser-automation) — Smart Search & Browser Automation
@@ -283,7 +304,7 @@ Companion repos to build a complete, self-running AI assistant:
283
304
  - [openclaw-n8n-facebook-poster](https://github.com/tuanminhhole/openclaw-n8n-facebook-poster) — Auto-post to Facebook via n8n
284
305
 
285
306
  **🧩 Skills**
286
- - [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
287
308
  - [openclaw-skill-infographic](https://github.com/tuanminhhole/openclaw-skill-infographic) — Infographic generation
288
309
  - [openclaw-skill-zalo-sticker-mention](https://github.com/tuanminhhole/openclaw-skill-zalo-sticker-mention) — Stickers & mentions on Zalo
289
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.0-0EA5E9?style=for-the-badge" alt="Version 5.10.0" /></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,26 @@
27
27
 
28
28
  ---
29
29
 
30
- ## 🆕 Có gì mới trong v5.10.0
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>
38
+
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.
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.
41
+ - 🔐 **Sync 9router lần đầu — đã sửa**: không còn tắt vĩnh viễn "Require login". `sync.js` đăng nhập bằng mật khẩu mặc định `123456`, tạo combo `smart-route` từ provider đang active **một lần** rồi dừng. Require login giữ ON (đổi mật khẩu sau); model-call `/v1` không bị ảnh hưởng.
42
+ - 📁 **Đường dẫn workspace native — đã sửa**: `workspace` của agent giờ là tương đối, persona/memory/skills nằm đúng chỗ khi cài native (trước là đường dẫn tuyệt đối kiểu container → trỏ sai trên host).
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.
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.
45
+
46
+ </details>
47
+
48
+ <details>
49
+ <summary><b>Trước đó: Có gì mới trong v5.9.0</b></summary>
31
50
 
32
51
  - 🧠 **TencentDB Agent Memory — cài 1 chạm**: Plugin bộ nhớ mới ngay trên UI. Bộ nhớ phân tầng 4 lớp (L0–L3) + nén ngữ cảnh, giữ session dài mạch lạc và **tiết kiệm tới ~61% token**. Chạy local hoàn toàn (SQLite), không cần API key, sẵn sàng cho Docker.
33
52
  - ⚡ **Cấu hình tiết kiệm token mặc định cho mọi bot mới**: Tự có context pruning (cache-TTL) + compaction `safeguard` — **hội thoại dài rẻ hơn & sắc nét hơn**, không cần chỉnh tay.
@@ -36,6 +55,8 @@
36
55
  - 🐳 **Nút điều khiển Docker 1 chạm**: Restart / Rebuild container bot và cấp quyền ổ đĩa (mount thư mục host bất kỳ vào `/mnt/<tên>`, đa OS) ngay trên dashboard — khởi động lại bot mà không cần gõ lệnh.
37
56
  - ⚡ **Trang Dashboard & Bot load nhanh hơn**: Việc dò runtime/version giờ chỉ chạy **một lần** rồi cache thay vì lặp lại mỗi lần tải trang — bot status giảm từ ~4s xuống ~3ms ở các lần tải sau. Cache tự xoá khi update/rebuild/restart/cài đặt.
38
57
 
58
+ </details>
59
+
39
60
  <details>
40
61
  <summary><b>Trước đó: Có gì mới trong v5.8.24 (Tự động cập nhật Launcher)</b></summary>
41
62
 
@@ -190,7 +211,7 @@ npx github:tuanminhhole/openclaw-setup
190
211
  - **Telegram**: Lấy token bot chính thức từ `@BotFather`.
191
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).
192
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.
193
- - **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.
194
215
  - **Discord**: _Sắp ra mắt._
195
216
  - **Lark**: _Sắp ra mắt._
196
217
 
@@ -275,7 +296,7 @@ Các repo đi kèm để bạn dựng một trợ lý AI "tự vận hành" hoà
275
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)
276
297
 
277
298
  **🔌 Plugin (runtime)**
278
- - `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
279
300
  - [openclaw-telegram-multibot-relay](https://github.com/tuanminhhole/openclaw-telegram-multibot-relay) — Multibot Telegram relay, delegation & cron nhắc lịch native
280
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)
281
302
  - [openclaw-browser-automation](https://github.com/tuanminhhole/openclaw-browser-automation) — Smart Search & Browser Automation
@@ -283,7 +304,7 @@ Các repo đi kèm để bạn dựng một trợ lý AI "tự vận hành" hoà
283
304
  - [openclaw-n8n-facebook-poster](https://github.com/tuanminhhole/openclaw-n8n-facebook-poster) — Tự động đăng Facebook qua n8n
284
305
 
285
306
  **🧩 Skill**
286
- - [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
287
308
  - [openclaw-skill-infographic](https://github.com/tuanminhhole/openclaw-skill-infographic) — Tạo infographic
288
309
  - [openclaw-skill-zalo-sticker-mention](https://github.com/tuanminhhole/openclaw-skill-zalo-sticker-mention) — Sticker & mention trên Zalo
289
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) {
@@ -2277,6 +2030,13 @@ async function syncDockerInfra(projectDir, force = false) {
2277
2030
 
2278
2031
  sendLog(`[sync] Updating Docker infrastructure files (v${existingVersion} \u2192 v${SETUP_VERSION})`);
2279
2032
  await fsp.writeFile(join(dockerDir, 'Dockerfile'), docker.dockerfile, 'utf8');
2033
+ // Capture the user's custom disk/folder mounts from the OLD compose before we overwrite it — a
2034
+ // full regen only re-emits the default volumes, so without this the bot loses granted drives.
2035
+ let carriedMounts = [];
2036
+ try {
2037
+ const prevCompose = join(dockerDir, 'docker-compose.yml');
2038
+ if (existsSync(prevCompose)) carriedMounts = parseComposeMounts(await fsp.readFile(prevCompose, 'utf8'));
2039
+ } catch {}
2280
2040
  await fsp.writeFile(join(dockerDir, 'docker-compose.yml'), newCompose, 'utf8');
2281
2041
  // Preserve zalo-mod dashboard port if plugin is active
2282
2042
  try {
@@ -2287,14 +2047,29 @@ async function syncDockerInfra(projectDir, force = false) {
2287
2047
  let cc = await fsp.readFile(join(dockerDir, 'docker-compose.yml'), 'utf8');
2288
2048
  if (!cc.includes(`:${dp}`)) {
2289
2049
  const gpStr = String(gatewayPort);
2050
+ // Match the gateway published-port line whatever the host prefix is — the generated form is
2051
+ // "127.0.0.1:<gw>:<gw>", so keying off the container port (":<gw>" before the quote) is the
2052
+ // only reliable anchor. The old `(?:\d+:)?` variant never matched the "127.0.0.1:" prefix.
2290
2053
  cc = cc.replace(
2291
- new RegExp(`^(\\s*-\\s*"(?:\\d+:)?${gpStr}(?::${gpStr})?"\\s*)$`, 'm'),
2054
+ new RegExp(`^(\\s*-\\s*"[^"\\n]*:${gpStr}")\\s*$`, 'm'),
2292
2055
  `$1\n - "127.0.0.1:${dp}:${dp}" # zalo-mod dashboard`
2293
2056
  );
2294
2057
  await fsp.writeFile(join(dockerDir, 'docker-compose.yml'), cc, 'utf8');
2295
2058
  }
2296
2059
  }
2297
2060
  } catch {}
2061
+ // Re-inject the user's custom mounts carried over from the old compose.
2062
+ if (carriedMounts.length) {
2063
+ try {
2064
+ const cpath = join(dockerDir, 'docker-compose.yml');
2065
+ const cur = await fsp.readFile(cpath, 'utf8');
2066
+ const merged = injectMountsIntoCompose(cur, carriedMounts);
2067
+ if (merged !== cur) {
2068
+ await fsp.writeFile(cpath, merged, 'utf8');
2069
+ sendLog(`[sync] Preserved ${carriedMounts.length} granted mount(s) across the compose regen`);
2070
+ }
2071
+ } catch (e) { sendLog(`[sync] Warning: could not re-apply granted mounts: ${e.message}`); }
2072
+ }
2298
2073
  await fsp.writeFile(entrypointPath, entryScript, 'utf8');
2299
2074
  if (docker.syncScript) await fsp.writeFile(join(dockerDir, 'sync.js'), docker.syncScript, 'utf8');
2300
2075
  if (docker.patchScript) await fsp.writeFile(join(dockerDir, 'patch-9router.js'), docker.patchScript, 'utf8');
@@ -2370,16 +2145,7 @@ async function updateRuntime(target, projectDir) {
2370
2145
  probeCacheClear();
2371
2146
  return { ok: true, target, spec, mode: 'docker' };
2372
2147
  }
2373
- await run('npm', ['install', '-g', spec]);
2374
- if (isRouter) {
2375
- await run('openclaw', ['gateway', 'stop'], { cwd: projectDir }).catch(() => {});
2376
- await run('npm', ['install', '-g', NINE_ROUTER_NPM_SPEC]);
2377
- } else {
2378
- await run('npm', ['install', '-g', OPENCLAW_NPM_SPEC]);
2379
- }
2380
- await syncRuntimeState(projectDir, { full: true }).catch(() => {});
2381
- probeCacheClear();
2382
- return { ok: true, target, spec, mode: 'native' };
2148
+ throw httpError(400, 'Không có project Docker để cập nhật runtime.');
2383
2149
  }
2384
2150
 
2385
2151
  async function restartDockerBotContainer(projectDir = state.projectDir) {
@@ -2392,6 +2158,45 @@ async function restartDockerBotContainer(projectDir = state.projectDir) {
2392
2158
  return true;
2393
2159
  }
2394
2160
 
2161
+ // Parse user-granted disk/folder mounts (/mnt/<name>, excluding the always-present /mnt/project)
2162
+ // from a docker-compose.yml string. Handles both the long-form bind (type/source/target) and the
2163
+ // legacy short form `- "<host>:/mnt/<name>"`.
2164
+ function parseComposeMounts(compose) {
2165
+ const mounts = [];
2166
+ const lines = String(compose || '').split('\n');
2167
+ for (let i = 0; i < lines.length; i++) {
2168
+ const tm = lines[i].match(/^\s*target:\s*['"]?(\/mnt\/[A-Za-z0-9._-]+)['"]?\s*$/);
2169
+ if (tm && tm[1] !== '/mnt/project') {
2170
+ let host = '';
2171
+ for (let j = i - 1; j >= Math.max(0, i - 3); j--) {
2172
+ const sm = lines[j].match(/^\s*source:\s*['"]?(.+?)['"]?\s*$/);
2173
+ if (sm) { host = sm[1].replace(/''/g, "'"); break; }
2174
+ }
2175
+ if (host) mounts.push({ host, target: tm[1] });
2176
+ continue;
2177
+ }
2178
+ const ssm = lines[i].match(/^\s*-\s*"?([^":\n]+):(\/mnt\/[A-Za-z0-9._-]+)"?\s*$/);
2179
+ if (ssm && ssm[2] !== '/mnt/project') mounts.push({ host: ssm[1].trim(), target: ssm[2] });
2180
+ }
2181
+ return mounts;
2182
+ }
2183
+
2184
+ // Insert long-form bind mounts (idempotent) after the project's .openclaw volume line. Returns the
2185
+ // original string unchanged if the anchor is missing or a mount is already present.
2186
+ function injectMountsIntoCompose(compose, mounts) {
2187
+ const anchor = /^(\s*-\s*\.\.\/\.\.\/\.openclaw:\/home\/node\/project\/\.openclaw)\s*$/m;
2188
+ if (!Array.isArray(mounts) || !mounts.length || !anchor.test(compose)) return compose;
2189
+ let out = compose;
2190
+ for (const m of mounts) {
2191
+ if (!m || !m.host || !m.target) continue;
2192
+ if (out.includes(`target: ${m.target}`) || out.includes(`:${m.target}"`) || out.includes(`:${m.target}\n`) || out.includes(m.host)) continue;
2193
+ const src = `'${String(m.host).replace(/'/g, "''")}'`;
2194
+ const block = ` - type: bind\n source: ${src}\n target: ${m.target}`;
2195
+ out = out.replace(anchor, `$1\n${block}`);
2196
+ }
2197
+ return out;
2198
+ }
2199
+
2395
2200
  // Grant the bot access to a host disk/folder by mounting it into the container at /mnt/<name>.
2396
2201
  // Project-scoped: all bots in this project share the container, so all of them can use it.
2397
2202
  // Per-bot limits are described in AGENTS.md (not enforced at the mount layer).
@@ -2399,7 +2204,10 @@ async function addBotMount(projectDir, hostPath, mountName = '') {
2399
2204
  // Cross-OS normalize: trim, convert Windows backslashes → forward slashes (Docker accepts
2400
2205
  // forward slashes on every OS, incl. `C:/Users/...`), drop trailing separators. This avoids
2401
2206
  // YAML backslash issues and keeps the path uniform.
2402
- const cleanPath = String(hostPath || '').trim().replace(/\\+/g, '/').replace(/\/+$/, '');
2207
+ let cleanPath = String(hostPath || '').trim().replace(/\\+/g, '/').replace(/\/+$/, '');
2208
+ // A bare Windows drive letter ("D:") is an INVALID Docker bind source — the trailing-slash strip
2209
+ // above turns "D:/" into "D:". Restore the slash so mounting a whole drive (e.g. D:\) works.
2210
+ if (/^[a-zA-Z]:$/.test(cleanPath)) cleanPath += '/';
2403
2211
  if (!cleanPath) throw httpError(400, 'Đường dẫn ổ đĩa/thư mục đang trống');
2404
2212
  const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
2405
2213
  if (!existsSync(composeFile)) throw httpError(400, 'Không tìm thấy docker-compose.yml (project có thể không chạy ở chế độ Docker)');
@@ -2540,6 +2348,192 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
2540
2348
  }
2541
2349
  }
2542
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
+
2543
2537
  async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, routerPort = 20128 }) {
2544
2538
  state.installing = true;
2545
2539
  state.installed = false;
@@ -2551,6 +2545,9 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
2551
2545
  try {
2552
2546
  sendLog('OpenClaw local installer started');
2553
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);
2554
2551
  await writeCoreProject({ projectDir, osChoice, mode, gatewayPort, routerPort });
2555
2552
  await run('npm', ['install', '-g', OPENCLAW_NPM_SPEC]);
2556
2553
  await run('npm', ['install', '-g', NINE_ROUTER_NPM_SPEC]);
@@ -2567,63 +2564,6 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
2567
2564
  await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
2568
2565
  await applyResolved9RouterApiKey(projectDir).catch(() => {});
2569
2566
  await recreateDockerBot(projectDir).catch(() => {});
2570
- } else {
2571
- const nineRouterDataDir = join(projectDir, '.9router');
2572
- const nineRouterDbPath = join(nineRouterDataDir, 'db', 'data.sqlite');
2573
- const runtimeEnv = {
2574
- OPENCLAW_HOME: join(projectDir, '.openclaw'),
2575
- OPENCLAW_STATE_DIR: join(projectDir, '.openclaw'),
2576
- DATA_DIR: nineRouterDataDir,
2577
- OPENCLAW_GATEWAY_PORT: String(gatewayPort),
2578
- OPENCLAW_PORT: String(gatewayPort),
2579
- // Parity with the docker runtime env (docker-gen.js) so the gateway and browser plugin
2580
- // behave the same natively: allow the private-network control-UI websocket and tell the
2581
- // browser plugin which host OS it is running on.
2582
- OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: '1',
2583
- OPENCLAW_SETUP_OS: osChoice || '',
2584
- OPENCLAW_BROWSER_HOST_OS: osChoice || '',
2585
- };
2586
- await run('openclaw', ['gateway', 'stop'], { cwd: projectDir, env: runtimeEnv }).catch(() => {});
2587
- // 9router binds loopback in native mode on purpose: openclaw runs on the SAME host and
2588
- // reaches it via localhost (see get9RouterBaseUrl), so there is no reason to expose the LLM
2589
- // proxy on 0.0.0.0 — even on a VPS that would be an open relay. Only the gateway is exposed.
2590
- // Start 9router first (auto-restart supervised), then sync, then the gateway — so the gateway
2591
- // boots after 9router is reachable and after the API key has been written to its config.
2592
- const routerMethod = await startNativeService({
2593
- projectDir, name: '9router', cmd: '9router', desc: `OpenClaw 9router (${nativeServiceId(projectDir)})`,
2594
- args: ['-n', '-l', '-H', '127.0.0.1', '-p', String(routerPort), '--skip-update'], env: runtimeEnv,
2595
- });
2596
- // Smart-route auto-sync: the docker sidecar runs a script that disables 9router's login
2597
- // requirement and builds the default `smart-route` combo from the active providers. Native
2598
- // mode skipped this, so the default model had no backing models. Reuse the exact docker
2599
- // script with the native DB path (passed via env to stay cross-platform / shell-safe).
2600
- try {
2601
- const artifacts = buildDockerArtifacts({ is9Router: true, osChoice, openClawNpmSpec: OPENCLAW_NPM_SPEC, gatewayPort, routerPort });
2602
- if (artifacts.syncScript) {
2603
- await fsp.mkdir(nineRouterDataDir, { recursive: true });
2604
- const syncPath = join(nineRouterDataDir, 'sync.js');
2605
- await fsp.writeFile(syncPath, artifacts.syncScript, 'utf8');
2606
- // Run the sync under supervision too (managed service) so it is torn down cleanly on
2607
- // delete instead of leaking an orphaned `node sync.js` process.
2608
- await startNativeService({
2609
- projectDir, name: 'sync', cmd: process.execPath, args: [syncPath],
2610
- desc: `OpenClaw 9router smart-route sync (${nativeServiceId(projectDir)})`,
2611
- env: { ...runtimeEnv, NINEROUTER_DB_PATH: nineRouterDbPath, PORT: String(routerPort) },
2612
- });
2613
- sendLog('[native] 9router smart-route sync started');
2614
- }
2615
- } catch (e) {
2616
- sendLog(`[native] smart-route sync setup skipped: ${e.message}`);
2617
- }
2618
- // Give 9router a moment to boot + sync to disable its login gate, then resolve its API key
2619
- // into openclaw.json BEFORE the gateway starts (so the gateway reads it on first boot).
2620
- await new Promise((r) => setTimeout(r, 8000));
2621
- await applyResolved9RouterApiKey(projectDir).catch(() => {});
2622
- const gatewayMethod = await startNativeService({
2623
- projectDir, name: 'gateway', cmd: 'openclaw', desc: `OpenClaw gateway (${nativeServiceId(projectDir)})`,
2624
- args: ['gateway', 'run'], env: runtimeEnv,
2625
- });
2626
- sendLog(`Native runtime started — 9router via ${routerMethod}, gateway via ${gatewayMethod}${state.botPid ? ` (pid=${state.botPid})` : ''}`);
2627
2567
  }
2628
2568
  state.installed = true;
2629
2569
  sendLog('✅ Install completed');
@@ -3028,10 +2968,6 @@ async function deleteProjectFolder(projectDir, rootProjectDir) {
3028
2968
  });
3029
2969
  // Sleep 2.5 seconds to let Windows file system release overlays/locks
3030
2970
  await new Promise((resolve) => setTimeout(resolve, 2500));
3031
- } else {
3032
- // Native project: stop & remove any launchd/systemd auto-restart services so they don't keep
3033
- // respawning a deleted bot. No-op when no services were registered.
3034
- await removeNativeAutostart(resolved);
3035
2971
  }
3036
2972
 
3037
2973
  try {
@@ -3260,7 +3196,7 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
3260
3196
  if (dashPort && !composeContent.includes(`:${dashPort}`)) {
3261
3197
  const gwPortStr = String(Number(cfg.gateway?.port) || state.gatewayPort || 18789);
3262
3198
  composeContent = composeContent.replace(
3263
- new RegExp(`^(\\s*-\\s*"(?:\\d+:)?${gwPortStr}(?::${gwPortStr})?"\\s*)$`, 'm'),
3199
+ new RegExp(`^(\\s*-\\s*"[^"\\n]*:${gwPortStr}")\\s*$`, 'm'),
3264
3200
  `$1\n - "127.0.0.1:${dashPort}:${dashPort}" # zalo-mod dashboard`
3265
3201
  );
3266
3202
  await fsp.writeFile(composeFile, composeContent, 'utf8');
@@ -3459,7 +3395,7 @@ async function installFeature(projectDir, agentId, kind, id) {
3459
3395
  // Insert dashboard port after the gateway port line
3460
3396
  const gwPortStr = String(gwPort);
3461
3397
  composeContent = composeContent.replace(
3462
- new RegExp(`^(\\s*-\\s*"(?:\\d+:)?${gwPortStr}(?::${gwPortStr})?"\\s*)$`, 'm'),
3398
+ new RegExp(`^(\\s*-\\s*"[^"\\n]*:${gwPortStr}")\\s*$`, 'm'),
3463
3399
  `$1\n - ${dashPortMapping} # zalo-mod dashboard`
3464
3400
  );
3465
3401
  await fsp.writeFile(composeFile, composeContent, 'utf8');
@@ -3920,6 +3856,9 @@ async function handler(req, res, rootProjectDir) {
3920
3856
  const projectDir = await resolveProjectDir(rootProjectDir, body);
3921
3857
  return json(res, await addBotMount(projectDir, body.hostPath, body.mountName));
3922
3858
  }
3859
+ if (url.pathname === '/api/browser/start-chrome-debug' && req.method === 'POST') {
3860
+ return json(res, await startChromeDebug());
3861
+ }
3923
3862
  if (url.pathname === '/api/setup/update' && req.method === 'POST') {
3924
3863
  const installerDir = resolve(__dirname, '../..');
3925
3864
  const isGit = existsSync(resolve(installerDir, '.git'));
@@ -4036,11 +3975,19 @@ async function handler(req, res, rootProjectDir) {
4036
3975
  return json(res, { name, content: await fsp.readFile(file, 'utf8') });
4037
3976
  }
4038
3977
  if (req.method === 'PUT') {
4039
- 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'})`);
4040
3982
  const body = await readJson(req);
4041
3983
  const projectDir = await resolveProjectDir(rootProjectDir, body);
4042
3984
  const file = safeJoin(projectDir, name);
4043
- 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');
4044
3991
  return json(res, { ok: true });
4045
3992
  }
4046
3993
  }
@@ -4254,7 +4201,7 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
4254
4201
  printRemoteAccessHint(port).catch(() => {});
4255
4202
  }
4256
4203
 
4257
- export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloUserLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, removeNativeAutostart };
4204
+ export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloUserLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder };
4258
4205
 
4259
4206
 
4260
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.0",
3
+ "version": "5.11.0",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {