create-openclaw-bot 5.15.1 → 5.15.2

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
@@ -7,7 +7,7 @@
7
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.15.1-0EA5E9?style=for-the-badge" alt="Version 5.15.1" /></a>
10
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.15.2-0EA5E9?style=for-the-badge" alt="Version 5.15.2" /></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>
@@ -37,7 +37,7 @@
37
37
 
38
38
  ---
39
39
 
40
- ## 🆕 What's New in v5.15.1
40
+ ## 🆕 What's New in v5.15.2
41
41
 
42
42
  - 🖥️ **"Control PC" — let the bot open Chrome & apps on your machine**: a new toggle on each bot card turns on the ability for the bot to open Chrome or an allow-listed app (TeamViewer, Zalo, …) on the computer running it. **Off by default**, token-gated, allow-list only, desktop-only.
43
43
  - 🌐 **Browsing works out of the box on every OS**: the bot drives your real Chrome on a desktop (your logged-in profile, so sites don't flag it) and starts its own headless Chromium on a server — no more "no browser available" just because Chrome wasn't running.
package/README.vi.md CHANGED
@@ -7,7 +7,7 @@
7
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.15.1-0EA5E9?style=for-the-badge" alt="Version 5.15.1" /></a>
10
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.15.2-0EA5E9?style=for-the-badge" alt="Version 5.15.2" /></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>
@@ -37,7 +37,7 @@
37
37
 
38
38
  ---
39
39
 
40
- ## 🆕 Có gì mới trong v5.15.1
40
+ ## 🆕 Có gì mới trong v5.15.2
41
41
 
42
42
  - 🖥️ **"Điều khiển máy" — cho bot mở Chrome & ứng dụng trên máy bạn**: nút mới trên thẻ mỗi bot, bật lên là bot mở được Chrome hoặc app trong danh sách cho phép (TeamViewer, Zalo, …) trên máy đang chạy bot. **Mặc định TẮT**, cần token, chỉ giới hạn danh sách bạn khai, chỉ dùng trên máy có màn hình.
43
43
  - 🌐 **Duyệt web chạy ngay trên mọi hệ điều hành**: máy để bàn thì bot dùng **Chrome thật** (profile đã đăng nhập, web ít nghi là bot), server thì tự mở Chromium headless — hết cảnh "không có trình duyệt" chỉ vì chưa mở Chrome.
@@ -1,5 +1,5 @@
1
1
  import http from 'http';
2
- import fs, { createReadStream, existsSync, promises as fsp } from 'fs';
2
+ import fs, { createReadStream, existsSync, readFileSync, promises as fsp } from 'fs';
3
3
  import { createRequire } from 'module';
4
4
  import { basename, dirname, extname, join, normalize, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
@@ -24,7 +24,7 @@ const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
24
24
  // scripts) runs a dedicated profile seeded from the operator's real one. Kept outside Chrome's
25
25
  // own folders: the block is an exact match on the default directory, and a sibling of it is a
26
26
  // needless bet on that staying true.
27
- const CHROME_SCRIPT_MARKER = 'OPENCLAW_CHROME_PROFILE_V2';
27
+ const CHROME_SCRIPT_MARKER = 'OPENCLAW_CHROME_PROFILE_V3';
28
28
  const CHROME_DEBUG_PROFILE_LEAF_WIN = 'OpenClaw\\chrome-profile';
29
29
  const CHROME_DEBUG_PROFILE_LEAF_MAC = 'Library/Application Support/OpenClaw/chrome-profile';
30
30
  const CHROME_DEBUG_PROFILE_LEAF_LINUX = '.config/openclaw/chrome-profile';
@@ -104,6 +104,15 @@ async function pushBrowserScriptsIntoContainer(projectDir, aliases, files, sendL
104
104
  return pushed;
105
105
  }
106
106
 
107
+ /**
108
+ * The browser-automation plugin defaults every high-impact behaviour to off (ClawHub's review reads
109
+ * broad defaults as "rogue agent", fairly). A dashboard install is an explicit, informed action, so
110
+ * the flags are turned on here — and stay visible in openclaw.json for anyone who wants them off.
111
+ */
112
+ function browserAutomationOptIns() {
113
+ return { patchDocker: true, allowPageScripting: true, allowFileUpload: true };
114
+ }
115
+
107
116
  async function patchBrowserAutomationHostPreference(projectDir, aliases = [], sendLog = () => {}) {
108
117
  const preferredCdpBlock = `const dns = require('dns').promises;
109
118
  const DEFAULT_CDP_URLS = [
@@ -200,9 +209,20 @@ async function connectPreferredChrome() {
200
209
  // the same way and does not care.
201
210
  'ping -n 3 127.0.0.1 >nul',
202
211
  '',
212
+ // Copying the real profile duplicates cookies, logins, history and extensions into a second
213
+ // directory and has to close every Chrome window to do it. That is the operator's call, not
214
+ // a silent default: without OPENCLAW_CHROME_SEED_PROFILE=1 this starts a clean profile and
215
+ // they sign in once, in the window that opens.
203
216
  'if not exist "%OPENCLAW_CHROME_PROFILE_DIR%\\Default" (',
204
217
  // No parentheses in text inside an if-block: cmd closes the block on the first ")".
205
- ' echo Lan dau: dang dong Chrome de chep profile - cookie, dang nhap...',
218
+ ' echo Chua co profile dieu khien - se tao MOI va ban dang nhap 1 lan trong cua so vua mo.',
219
+ ' echo Muon dung san dang nhap cua Chrome thuong thi dat OPENCLAW_CHROME_SEED_PROFILE=1 roi chay lai.',
220
+ ' echo Luu y: viec do CHEP cookie, dang nhap, lich su, extension sang profile dieu khien',
221
+ ' echo va phai DONG het cua so Chrome dang mo de chep.',
222
+ ')',
223
+ '',
224
+ 'if not exist "%OPENCLAW_CHROME_PROFILE_DIR%\\Default" if "%OPENCLAW_CHROME_SEED_PROFILE%"=="1" (',
225
+ ' echo Dang dong Chrome de chep profile theo yeu cau cua ban...',
206
226
  ' taskkill /F /IM chrome.exe >nul 2>&1',
207
227
  ' ping -n 4 127.0.0.1 >nul',
208
228
  ' echo Dang chep profile Chrome that sang "%OPENCLAW_CHROME_PROFILE_DIR%" ...',
@@ -214,7 +234,9 @@ async function connectPreferredChrome() {
214
234
  'echo Dang mo Chrome - profile: %OPENCLAW_CHROME_PROFILE_DIR%',
215
235
  'start "" "%CHROME_BIN%" ^',
216
236
  ' --remote-debugging-port=9222 ^',
217
- ' --remote-allow-origins=* ^',
237
+ // Loopback only, and no --remote-allow-origins=*: a CDP client written in Node sends no
238
+ // Origin header, so the wildcard bought nothing and only widened who could drive Chrome.
239
+ ' --remote-debugging-address=127.0.0.1 ^',
218
240
  ' --user-data-dir="%OPENCLAW_CHROME_PROFILE_DIR%" ^',
219
241
  ' --profile-directory=Default ^',
220
242
  ' --no-first-run ^',
@@ -254,7 +276,8 @@ async function connectPreferredChrome() {
254
276
  'fi',
255
277
  '',
256
278
  '# Chrome 136+ refuses --remote-debugging-port on the default profile directory, so run a',
257
- '# dedicated one seeded from the real profile (keeps cookies, logins, history, extensions).',
279
+ '# dedicated one. It starts empty: copying the real profile duplicates cookies and logins,',
280
+ '# so it only happens when the operator asks with OPENCLAW_CHROME_SEED_PROFILE=1.',
258
281
  ': "${OPENCLAW_CHROME_PROFILE_DIR:=$DEFAULT_DEBUG_PROFILE}"',
259
282
  '',
260
283
  'echo "Using: $CHROME_BIN"',
@@ -262,21 +285,28 @@ async function connectPreferredChrome() {
262
285
  'pkill -f -- "--remote-debugging-port=9222" 2>/dev/null || true',
263
286
  'sleep 2',
264
287
  '',
265
- 'if [ ! -d "$OPENCLAW_CHROME_PROFILE_DIR/Default" ] && [ -d "$REAL_PROFILE/Default" ]; then',
266
- ' echo "First run: copying the real Chrome profile into $OPENCLAW_CHROME_PROFILE_DIR ..."',
267
- ' mkdir -p "$OPENCLAW_CHROME_PROFILE_DIR/Default"',
268
- ' cp -R "$REAL_PROFILE/Default/." "$OPENCLAW_CHROME_PROFILE_DIR/Default/" 2>/dev/null || true',
269
- ' cp -f "$REAL_PROFILE/Local State" "$OPENCLAW_CHROME_PROFILE_DIR/Local State" 2>/dev/null || true',
270
- ` for junk in ${chromeProfileCacheJunk.map((n) => `"${n}"`).join(' ')}; do`,
271
- ' rm -rf "$OPENCLAW_CHROME_PROFILE_DIR/Default/$junk"',
272
- ' done',
288
+ 'if [ ! -d "$OPENCLAW_CHROME_PROFILE_DIR/Default" ]; then',
289
+ ' if [ "$OPENCLAW_CHROME_SEED_PROFILE" = "1" ] && [ -d "$REAL_PROFILE/Default" ]; then',
290
+ ' echo "Copying your real Chrome profile into $OPENCLAW_CHROME_PROFILE_DIR as requested"',
291
+ ' echo "(cookies, logins, history and extensions are duplicated into that directory)."',
292
+ ' mkdir -p "$OPENCLAW_CHROME_PROFILE_DIR/Default"',
293
+ ' cp -R "$REAL_PROFILE/Default/." "$OPENCLAW_CHROME_PROFILE_DIR/Default/" 2>/dev/null || true',
294
+ ' cp -f "$REAL_PROFILE/Local State" "$OPENCLAW_CHROME_PROFILE_DIR/Local State" 2>/dev/null || true',
295
+ ` for junk in ${chromeProfileCacheJunk.map((n) => `"${n}"`).join(' ')}; do`,
296
+ ' rm -rf "$OPENCLAW_CHROME_PROFILE_DIR/Default/$junk"',
297
+ ' done',
298
+ ' else',
299
+ ' echo "Starting a clean automation profile - sign in once in the window that opens."',
300
+ ' echo "To reuse your existing logins instead: OPENCLAW_CHROME_SEED_PROFILE=1 $0"',
301
+ ' echo "(that copies cookies, logins, history and extensions into the automation profile)."',
302
+ ' fi',
273
303
  'fi',
274
304
  'mkdir -p "$OPENCLAW_CHROME_PROFILE_DIR"',
275
305
  '',
276
306
  'echo "Starting Chrome (profile: $OPENCLAW_CHROME_PROFILE_DIR)..."',
277
307
  '"$CHROME_BIN" \\',
278
308
  ' --remote-debugging-port=9222 \\',
279
- ' --remote-allow-origins=* \\',
309
+ ' --remote-debugging-address=127.0.0.1 \\',
280
310
  ' --user-data-dir="$OPENCLAW_CHROME_PROFILE_DIR" \\',
281
311
  ' --profile-directory=Default \\',
282
312
  ' --no-first-run \\',
@@ -690,17 +720,58 @@ function run(cmd, args, opts = {}) {
690
720
  });
691
721
  }
692
722
 
723
+ // Where npm puts global packages, derived from the running node binary so we never pay for
724
+ // `npm root -g` (~100ms) just to find a version string.
725
+ function globalNodeModulesDirs() {
726
+ const dirs = [];
727
+ const nodeDir = dirname(process.execPath);
728
+ dirs.push(join(nodeDir, '..', 'lib', 'node_modules')); // unix prefix layout
729
+ dirs.push(join(nodeDir, 'node_modules')); // windows npm prefix
730
+ if (process.env.APPDATA) dirs.push(join(process.env.APPDATA, 'npm', 'node_modules'));
731
+ dirs.push('/usr/local/lib/node_modules', '/usr/lib/node_modules', '/opt/homebrew/lib/node_modules');
732
+ return dirs;
733
+ }
734
+
735
+ // `9router --version` boots the whole CLI and takes ~4 SECONDS on a normal machine. /api/system
736
+ // used to pay that on every single call — and the UI calls it after every action, so the whole
737
+ // dashboard felt slow for one version string. The version is right there in package.json.
738
+ function readGlobalPackageVersion(name) {
739
+ for (const dir of globalNodeModulesDirs()) {
740
+ const pkg = join(dir, name, 'package.json');
741
+ try {
742
+ if (!existsSync(pkg)) continue;
743
+ const version = String(JSON.parse(readFileSync(pkg, 'utf8')).version || '').trim();
744
+ if (version) return version;
745
+ } catch {}
746
+ }
747
+ return '';
748
+ }
749
+
693
750
  async function getCurrentRuntimeVersions() {
694
- const [openclaw, nineRouter, node] = await Promise.all([
695
- commandExists('openclaw', ['--version']),
696
- commandExists('9router', ['--version']),
697
- commandExists('node', ['--version']),
698
- ]);
699
- return {
700
- openclaw: openclaw.ok ? (openclaw.output.split(/\r?\n/)[0] || '').trim() : '',
701
- nineRouter: nineRouter.ok ? (nineRouter.output.split(/\r?\n/)[0] || '').trim() : '',
702
- node: node.ok ? (node.output.split(/\r?\n/)[0] || '').trim() : process.version,
751
+ const ck = 'hostver:runtimes';
752
+ const cached = probeCacheGet(ck);
753
+ if (cached) return cached;
754
+
755
+ const fromDisk = {
756
+ openclaw: readGlobalPackageVersion('openclaw'),
757
+ nineRouter: readGlobalPackageVersion('9router'),
758
+ node: process.version || '',
703
759
  };
760
+ // Only shell out for what disk did not answer — a global install in a prefix we do not know
761
+ // about, mostly. Still cached, so an odd layout costs the slow probe once, not every request.
762
+ const needCli = !fromDisk.openclaw || !fromDisk.nineRouter;
763
+ if (needCli) {
764
+ const [openclaw, nineRouter] = await Promise.all([
765
+ fromDisk.openclaw ? null : commandExists('openclaw', ['--version']),
766
+ fromDisk.nineRouter ? null : commandExists('9router', ['--version']),
767
+ ]);
768
+ if (openclaw?.ok) fromDisk.openclaw = (openclaw.output.split(/\r?\n/)[0] || '').trim();
769
+ if (nineRouter?.ok) fromDisk.nineRouter = (nineRouter.output.split(/\r?\n/)[0] || '').trim();
770
+ }
771
+ // Versions only change on an install/update, and those paths already call probeCacheClear().
772
+ // The TTL is just a backstop for a package installed behind this server's back.
773
+ probeCacheSet(ck, fromDisk, 10 * 60 * 1000);
774
+ return fromDisk;
704
775
  }
705
776
 
706
777
  // Per-project cache for EXPENSIVE runtime/version probes (docker exec + openclaw CLI). These
@@ -716,8 +787,35 @@ function probeCacheGet(key) {
716
787
  }
717
788
  function probeCacheSet(key, value, ttlMs = 0) { _probeCache.set(key, { value, exp: ttlMs ? Date.now() + ttlMs : 0 }); }
718
789
  function probeCacheClear(prefix = '') {
719
- if (!prefix) { _probeCache.clear(); return; }
790
+ if (!prefix) { _probeCache.clear(); _probeInflight.clear(); return; }
720
791
  for (const k of [..._probeCache.keys()]) if (k.startsWith(prefix)) _probeCache.delete(k);
792
+ for (const k of [..._probeInflight.keys()]) if (k.startsWith(prefix)) _probeInflight.delete(k);
793
+ }
794
+
795
+ // One probe per key at a time. The dashboard fires several requests at once and the startup
796
+ // prefetch runs alongside them, so without this the same docker/CLI round-trip ran two or three
797
+ // times over and every caller waited for the slowest copy. Serves a warm value immediately and
798
+ // refreshes in the background once it is half-stale, so a click never waits on a probe.
799
+ const _probeInflight = new Map();
800
+ function sharedProbe(key, ttlMs, compute) {
801
+ const cached = probeCacheGet(key);
802
+ if (cached) {
803
+ if (Date.now() - cached.at > ttlMs / 2 && !_probeInflight.has(key)) {
804
+ const bg = compute()
805
+ .then((value) => { probeCacheSet(key, { value, at: Date.now() }, ttlMs); return value; })
806
+ .finally(() => _probeInflight.delete(key));
807
+ _probeInflight.set(key, bg);
808
+ bg.catch(() => {});
809
+ }
810
+ return Promise.resolve(cached.value);
811
+ }
812
+ const existing = _probeInflight.get(key);
813
+ if (existing) return existing;
814
+ const run = compute()
815
+ .then((value) => { probeCacheSet(key, { value, at: Date.now() }, ttlMs); return value; })
816
+ .finally(() => _probeInflight.delete(key));
817
+ _probeInflight.set(key, run);
818
+ return run;
721
819
  }
722
820
 
723
821
  async function resolveProjectRuntimeVersions(projectDir, mode = state.mode || 'docker') {
@@ -2063,6 +2161,8 @@ async function waitForNativeGatewayZaloReady(projectDir, timeoutMs = 90000, chan
2063
2161
  }
2064
2162
 
2065
2163
  async function startZaloLogin(projectDir, agentId = "") {
2164
+ // A fresh login changes what channels status reports; do not serve the cached "not connected".
2165
+ probeCacheClear(`zalohealth:${projectDir || ''}`);
2066
2166
  const cfgPath = join(projectDir, ".openclaw", "openclaw.json");
2067
2167
  if (!existsSync(cfgPath)) throw httpError(404, "openclaw.json not found");
2068
2168
  const cfg = JSON.parse(await fsp.readFile(cfgPath, "utf8"));
@@ -2321,7 +2421,17 @@ function buildZaloHealthSnapshot(cfg = {}, statusJson = null, credentialNames =
2321
2421
  // ── Zalo health snapshot for the dashboard ──────────────────────────────────────
2322
2422
  // Runtime JSON is authoritative and account-aware. Text parsing remains only as a
2323
2423
  // compatibility fallback for older OpenClaw builds.
2324
- async function getZaloHealth(projectDir) {
2424
+ // `openclaw channels status` is a CLI round-trip (docker exec on a container, or the host
2425
+ // gateway) and costs ~3 seconds. The dashboard asks for this on every page load and after every
2426
+ // action, so it is cached for a few seconds and refreshed in the background: a second visit is
2427
+ // instant, and the number on screen is never more than a few seconds stale. Login/restart paths
2428
+ // clear it (probeCacheClear) so a state change shows up immediately.
2429
+ const ZALO_HEALTH_TTL_MS = 4000;
2430
+ function getZaloHealth(projectDir) {
2431
+ return sharedProbe(`zalohealth:${projectDir || ''}`, ZALO_HEALTH_TTL_MS, () => computeZaloHealth(projectDir));
2432
+ }
2433
+
2434
+ async function computeZaloHealth(projectDir) {
2325
2435
  const meta = {
2326
2436
  supportedVersion: ZALO_CONNECT_VERSION,
2327
2437
  installedVersion: null,
@@ -2338,19 +2448,24 @@ async function getZaloHealth(projectDir) {
2338
2448
  if (meta.zaloModVersion) meta.zaloModInstalled = true;
2339
2449
 
2340
2450
  const botContainer = getBotContainerName(projectDir);
2451
+ const native = isNativeProject(projectDir);
2341
2452
  if (cfg.channels?.['zalo-connect']?.enabled) {
2342
2453
  const manifestHost = join(projectDir, '.openclaw', 'extensions', 'zalo-connect', 'openclaw.plugin.json');
2343
2454
  try {
2344
2455
  meta.installedVersion = JSON.parse(await fsp.readFile(manifestHost, 'utf8')).version || null;
2345
2456
  } catch {
2346
- try {
2347
- const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'cat "${OPENCLAW_HOME:-/home/node/project/.openclaw}/extensions/zalo-connect/openclaw.plugin.json" 2>/dev/null'], { cwd: projectDir, shell: false, timeout: 8000 });
2348
- meta.installedVersion = JSON.parse(String(r.stdout || '{}')).version || null;
2349
- } catch {}
2457
+ // Only Docker projects keep the manifest inside a container. A native project has no
2458
+ // container at all, and this fallback used to shell into one anyway seconds of waiting on
2459
+ // a `docker exec` that could never succeed, on a request the dashboard makes constantly.
2460
+ if (!native) {
2461
+ try {
2462
+ const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'cat "${OPENCLAW_HOME:-/home/node/project/.openclaw}/extensions/zalo-connect/openclaw.plugin.json" 2>/dev/null'], { cwd: projectDir, shell: false, timeout: 8000 });
2463
+ meta.installedVersion = JSON.parse(String(r.stdout || '{}')).version || null;
2464
+ } catch {}
2465
+ }
2350
2466
  }
2351
2467
  }
2352
2468
 
2353
- const native = isNativeProject(projectDir);
2354
2469
  let containerRunning = false;
2355
2470
  let statusJson = null;
2356
2471
  let textStatus = '';
@@ -2360,7 +2475,9 @@ async function getZaloHealth(projectDir) {
2360
2475
  // /health over loopback, then read channel status + credentials directly on the host.
2361
2476
  const nmeta = readNativeMeta(projectDir) || {};
2362
2477
  const port = String(nmeta.gatewayPort || state.gatewayPort || NATIVE_DEFAULT_GATEWAY_PORT);
2363
- containerRunning = await probeHttpOk(`http://127.0.0.1:${port}/health`, 2500);
2478
+ // Loopback: a live gateway answers in milliseconds. The old 2.5s budget was pure waiting
2479
+ // whenever the runtime was down, on a request the dashboard makes on every page load.
2480
+ containerRunning = await probeHttpOk(`http://127.0.0.1:${port}/health`, 900);
2364
2481
  if (containerRunning) {
2365
2482
  try {
2366
2483
  const r = await ocCapture(projectDir, ['channels', 'status', '--json'], { timeout: 20000 });
@@ -3817,7 +3934,8 @@ async function ensureChromeRelay() {
3817
3934
  // Launch real host Chrome in remote-debugging mode (port 9222) so the browser-automation plugin
3818
3935
  // can drive the user's actual Chrome (logged-in profile) instead of headless Chromium. The bot
3819
3936
  // reaches it via CDP (host.docker.internal:9222 from the container). Detached: keeps running after
3820
- // this request. `--remote-allow-origins=*` is required by modern Chrome for cross-origin CDP.
3937
+ // this request. The debug port stays on loopback and no origin wildcard is passed — a Node CDP
3938
+ // client sends no Origin header, so the wildcard only widened who could drive the browser.
3821
3939
  // On a headless VPS there is no Chrome to open here — instead we start the bridge relay and hand
3822
3940
  // back copy-paste commands so the user runs Chrome on THEIR machine + a reverse SSH tunnel.
3823
3941
  // Where Chrome keeps the operator's own profile, per OS. Chrome must not already be running
@@ -3902,8 +4020,8 @@ async function startChromeDebug() {
3902
4020
  port: 9222,
3903
4021
  // Same dedicated profile directories the local button and the generated scripts use, so
3904
4022
  // an operator who has already run one of those keeps the session they signed in with.
3905
- chromeCmdMac: `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222 --user-data-dir="$HOME/${CHROME_DEBUG_PROFILE_LEAF_MAC}" --profile-directory=Default --remote-allow-origins='*'`,
3906
- chromeCmdWin: `"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%LOCALAPPDATA%\\${CHROME_DEBUG_PROFILE_LEAF_WIN}" --profile-directory=Default --remote-allow-origins=*`,
4023
+ chromeCmdMac: `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222 --user-data-dir="$HOME/${CHROME_DEBUG_PROFILE_LEAF_MAC}" --profile-directory=Default --remote-debugging-address=127.0.0.1`,
4024
+ chromeCmdWin: `"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222 --user-data-dir="%LOCALAPPDATA%\\${CHROME_DEBUG_PROFILE_LEAF_WIN}" --profile-directory=Default --remote-debugging-address=127.0.0.1`,
3907
4025
  tunnelCmd: `ssh -N -R 9222:127.0.0.1:9222 ${user}@${ip}`,
3908
4026
  };
3909
4027
  }
@@ -3922,12 +4040,20 @@ async function startChromeDebug() {
3922
4040
  // --enable-automation, so navigator.webdriver stays false and there is no banner.
3923
4041
  // Set OPENCLAW_CHROME_PROFILE_DIR to point somewhere else (anything but the default profile).
3924
4042
  const userDataDir = process.env.OPENCLAW_CHROME_PROFILE_DIR || debugChromeProfileDir();
3925
- await seedDebugChromeProfile(defaultChromeProfileDir(), userDataDir, sendLog);
4043
+ // Only when the operator asked for their logins: the copy duplicates cookies and sessions into
4044
+ // a second profile directory, which is not something to do on a button press by default.
4045
+ if (process.env.OPENCLAW_CHROME_SEED_PROFILE === '1') {
4046
+ await seedDebugChromeProfile(defaultChromeProfileDir(), userDataDir, sendLog);
4047
+ } else if (!existsSync(join(userDataDir, 'Default'))) {
4048
+ sendLog('[chrome] Mở Chrome với profile điều khiển trống — đăng nhập 1 lần trong cửa sổ vừa mở. Muốn dùng sẵn đăng nhập của Chrome thường thì đặt OPENCLAW_CHROME_SEED_PROFILE=1 (sẽ chép cookie/đăng nhập/lịch sử sang profile đó).');
4049
+ }
3926
4050
  const args = [
3927
4051
  `--remote-debugging-port=${port}`,
4052
+ // Loopback only, and no --remote-allow-origins=*: a Node CDP client sends no Origin header,
4053
+ // so the wildcard only widened who could drive this browser.
4054
+ '--remote-debugging-address=127.0.0.1',
3928
4055
  `--user-data-dir=${userDataDir}`,
3929
4056
  '--profile-directory=Default',
3930
- '--remote-allow-origins=*',
3931
4057
  '--no-first-run',
3932
4058
  '--no-default-browser-check',
3933
4059
  ];
@@ -4145,6 +4271,9 @@ async function listMarkdownFiles(projectDir, agentId = '') {
4145
4271
  }
4146
4272
 
4147
4273
  async function saveState(rootProjectDir) {
4274
+ // Selecting, adding or removing a project all end up here, and all of them make the cached
4275
+ // project list wrong — drop it so the next request rebuilds instead of showing the old set.
4276
+ probeCacheClear('projects:');
4148
4277
  const file = join(rootProjectDir, STATE_FILE);
4149
4278
  await fsp.writeFile(file, JSON.stringify({
4150
4279
  projectDir: state.projectDir,
@@ -4226,6 +4355,9 @@ function isRestrictedSystemDir(dirPath) {
4226
4355
  // so a fresh `npx github:…` run (e.g. on a VPS where bots are already running) targets the
4227
4356
  // live project instead of defaulting to an empty ~/openclaw-setup folder.
4228
4357
  async function discoverDockerBotProjectRoots() {
4358
+ const ck = 'dockerroots';
4359
+ const cached = probeCacheGet(ck);
4360
+ if (cached) return cached;
4229
4361
  const roots = [];
4230
4362
  try {
4231
4363
  const r = await runCapture(
@@ -4244,7 +4376,9 @@ async function discoverDockerBotProjectRoots() {
4244
4376
  }
4245
4377
  }
4246
4378
  } catch {}
4247
- return [...new Set(roots)];
4379
+ const unique = [...new Set(roots)];
4380
+ probeCacheSet(ck, unique, 15000);
4381
+ return unique;
4248
4382
  }
4249
4383
 
4250
4384
  // Native installs have no container to inspect, so we can't detect them the way Docker bots are
@@ -4343,7 +4477,15 @@ async function ensureProjectsLoaded(rootProjectDir) {
4343
4477
  }
4344
4478
  }
4345
4479
 
4346
- async function discoverProjects(rootProjectDir) {
4480
+ // The project list costs docker/native probes per project. It changes when someone creates or
4481
+ // deletes a project — not between two page loads — so serve it from a short cache and refresh in
4482
+ // the background: the dashboard opens instantly and is at most a few seconds stale.
4483
+ const PROJECTS_TTL_MS = 10000;
4484
+ function discoverProjects(rootProjectDir) {
4485
+ return sharedProbe(`projects:${rootProjectDir || ''}`, PROJECTS_TTL_MS, () => computeDiscoverProjects(rootProjectDir));
4486
+ }
4487
+
4488
+ async function computeDiscoverProjects(rootProjectDir) {
4347
4489
  await ensureProjectsLoaded(rootProjectDir);
4348
4490
 
4349
4491
  // Surface projects whose bot is running in Docker even if this install has no saved
@@ -4371,16 +4513,14 @@ async function discoverProjects(rootProjectDir) {
4371
4513
  }
4372
4514
  }
4373
4515
 
4374
- const updatedProjects = [];
4375
- for (const p of state.projects) {
4376
- if (existsSync(join(p.projectDir, '.openclaw', 'openclaw.json'))) {
4377
- const meta = await buildProjectMeta(p.projectDir).catch(() => null);
4378
- if (meta) {
4379
- updatedProjects.push(meta);
4380
- }
4381
- }
4382
- }
4383
- state.projects = updatedProjects;
4516
+ // In parallel: each buildProjectMeta runs runtime detection (docker calls, port probes), so a
4517
+ // handful of projects turned into seconds of dashboard load when this was a sequential loop.
4518
+ const metas = await Promise.all(
4519
+ state.projects
4520
+ .filter((p) => existsSync(join(p.projectDir, '.openclaw', 'openclaw.json')))
4521
+ .map((p) => buildProjectMeta(p.projectDir).catch(() => null)),
4522
+ );
4523
+ state.projects = metas.filter(Boolean);
4384
4524
 
4385
4525
  state.projects.sort((a, b) => {
4386
4526
  const aActive = state.projectDir && resolve(state.projectDir) === resolve(a.projectDir);
@@ -4969,6 +5109,11 @@ async function installFeature(projectDir, agentId, kind, id) {
4969
5109
  if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
4970
5110
  cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
4971
5111
  hostOs: await resolveProjectHostOs(projectDir),
5112
+ // The plugin ships these off: editing the Docker build files, running page JavaScript
5113
+ // and uploading local files are things it will not do until an operator says so.
5114
+ // Installing it from this dashboard IS that operator saying so — otherwise browsing
5115
+ // would need a hand-edited config right after a one-click install.
5116
+ ...browserAutomationOptIns(),
4972
5117
  });
4973
5118
  cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
4974
5119
  cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:web']));
@@ -5068,6 +5213,11 @@ async function installFeature(projectDir, agentId, kind, id) {
5068
5213
  if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
5069
5214
  cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
5070
5215
  hostOs: await resolveProjectHostOs(projectDir),
5216
+ // The plugin ships these off: editing the Docker build files, running page JavaScript
5217
+ // and uploading local files are things it will not do until an operator says so.
5218
+ // Installing it from this dashboard IS that operator saying so — otherwise browsing
5219
+ // would need a hand-edited config right after a one-click install.
5220
+ ...browserAutomationOptIns(),
5071
5221
  });
5072
5222
  cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
5073
5223
  cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:web']));
@@ -5923,6 +6073,15 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
5923
6073
  // Bring the host-control service back up when the operator left it enabled, so the bot's
5924
6074
  // saved instructions keep working across installer restarts.
5925
6075
  ensureHostControl(projectDir).catch(() => {});
6076
+ // Warm the probes the first page load would otherwise wait on (project list, runtime versions,
6077
+ // public IP, Zalo status). They run while the browser is still starting, so the dashboard opens
6078
+ // against a warm cache instead of paying for docker and CLI round-trips on first paint.
6079
+ Promise.all([
6080
+ discoverProjects(projectDir).catch(() => {}),
6081
+ getCurrentRuntimeVersions().catch(() => {}),
6082
+ getPublicIp().catch(() => {}),
6083
+ existsSync(join(projectDir, '.openclaw', 'openclaw.json')) ? getZaloHealth(projectDir).catch(() => {}) : null,
6084
+ ]).catch(() => {});
5926
6085
  }
5927
6086
 
5928
6087
  export { patchBrowserAutomationHostPreference, debugChromeProfileDir, defaultChromeProfileDir, createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, buildZaloHealthSnapshot, removeEmptyWorkspaceAttestations, runHostCommand, detectHostCommands, detectHostCapabilityCommands, grantHostCapabilities, detectCodexApp, detectCodexMarketplace, resolveCodexCli, openPrivacyPane, projectDeployMode, isNativeProject, nativeServiceLabel, nativeEnv, ocArgv, migrateNativePaths, discoverNativeProjectRoots };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.15.1",
3
+ "version": "5.15.2",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {