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 +2 -2
- package/README.vi.md +2 -2
- package/dist/server/local-server.js +207 -48
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 = '
|
|
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
|
|
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
|
-
|
|
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
|
|
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" ]
|
|
266
|
-
'
|
|
267
|
-
'
|
|
268
|
-
'
|
|
269
|
-
'
|
|
270
|
-
|
|
271
|
-
'
|
|
272
|
-
'
|
|
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-
|
|
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
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
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 };
|