create-openclaw-bot 5.15.0 → 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 +278 -64
- package/dist/web/app.js +6 -4
- 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';
|
|
@@ -61,6 +61,58 @@ async function syncExecApprovals(projectDir, cfg) {
|
|
|
61
61
|
await fsp.writeFile(path2, JSON.stringify(approvals, null, 2), 'utf8');
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Write files into the bot container's plugin folder. Needed when `.openclaw/extensions` is a
|
|
66
|
+
* Docker named volume: the host sees an empty directory, so there is nothing on disk to patch.
|
|
67
|
+
* Best-effort — a stopped container or a project without Docker just yields 0.
|
|
68
|
+
*/
|
|
69
|
+
async function pushBrowserScriptsIntoContainer(projectDir, aliases, files, sendLog = () => {}) {
|
|
70
|
+
if (isNativeProject(projectDir)) return 0;
|
|
71
|
+
const container = getBotContainerName(projectDir);
|
|
72
|
+
if (!container) return 0;
|
|
73
|
+
const running = await runCapture('docker', ['inspect', '-f', '{{.State.Running}}', container], { shell: false, timeout: 8000 }).catch(() => null);
|
|
74
|
+
if (String(running?.stdout || '').trim() !== 'true') return 0;
|
|
75
|
+
|
|
76
|
+
const homeOut = await runCapture('docker', ['exec', container, 'sh', '-c', 'echo "${OPENCLAW_HOME:-/home/node/project/.openclaw}"'], { shell: false, timeout: 8000 }).catch(() => null);
|
|
77
|
+
const openclawHome = String(homeOut?.stdout || '').trim() || '/home/node/project/.openclaw';
|
|
78
|
+
|
|
79
|
+
let pushed = 0;
|
|
80
|
+
for (const alias of aliases) {
|
|
81
|
+
const dir = `${openclawHome}/extensions/${alias}`;
|
|
82
|
+
const check = await runCapture('docker', ['exec', container, 'sh', '-c', `[ -d "${dir}" ] && echo yes || echo no`], { shell: false, timeout: 8000 }).catch(() => null);
|
|
83
|
+
if (String(check?.stdout || '').trim() !== 'yes') continue;
|
|
84
|
+
for (const [name, content, mode] of files) {
|
|
85
|
+
const tmp = join(os.tmpdir(), `openclaw-${Date.now()}-${name}`);
|
|
86
|
+
try {
|
|
87
|
+
await fsp.writeFile(tmp, content, 'utf8');
|
|
88
|
+
// runCapture, not run: run() forces a shell on Windows, and the temp path goes through
|
|
89
|
+
// a home directory that usually has a space in it ("VT 2025") — cmd then splits it and
|
|
90
|
+
// docker cp fails with a usage error.
|
|
91
|
+
const cp = await runCapture('docker', ['cp', tmp, `${container}:${dir}/${name}`], { shell: false, timeout: 20000 });
|
|
92
|
+
if (cp.code !== 0) throw new Error(String(cp.stderr || cp.stdout || 'docker cp failed').trim());
|
|
93
|
+
// A world-writable file makes OpenClaw refuse to load the plugin, and a copy landing
|
|
94
|
+
// from a Windows host is exactly that.
|
|
95
|
+
await runCapture('docker', ['exec', container, 'sh', '-c', `chmod ${mode} "${dir}/${name}"`], { shell: false, timeout: 20000 });
|
|
96
|
+
pushed += 1;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
sendLog(`[browser] Could not push ${name} into ${container}: ${err.message}`);
|
|
99
|
+
} finally {
|
|
100
|
+
await fsp.rm(tmp, { force: true }).catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return pushed;
|
|
105
|
+
}
|
|
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
|
+
|
|
64
116
|
async function patchBrowserAutomationHostPreference(projectDir, aliases = [], sendLog = () => {}) {
|
|
65
117
|
const preferredCdpBlock = `const dns = require('dns').promises;
|
|
66
118
|
const DEFAULT_CDP_URLS = [
|
|
@@ -157,9 +209,20 @@ async function connectPreferredChrome() {
|
|
|
157
209
|
// the same way and does not care.
|
|
158
210
|
'ping -n 3 127.0.0.1 >nul',
|
|
159
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.
|
|
160
216
|
'if not exist "%OPENCLAW_CHROME_PROFILE_DIR%\\Default" (',
|
|
161
217
|
// No parentheses in text inside an if-block: cmd closes the block on the first ")".
|
|
162
|
-
' 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...',
|
|
163
226
|
' taskkill /F /IM chrome.exe >nul 2>&1',
|
|
164
227
|
' ping -n 4 127.0.0.1 >nul',
|
|
165
228
|
' echo Dang chep profile Chrome that sang "%OPENCLAW_CHROME_PROFILE_DIR%" ...',
|
|
@@ -171,7 +234,9 @@ async function connectPreferredChrome() {
|
|
|
171
234
|
'echo Dang mo Chrome - profile: %OPENCLAW_CHROME_PROFILE_DIR%',
|
|
172
235
|
'start "" "%CHROME_BIN%" ^',
|
|
173
236
|
' --remote-debugging-port=9222 ^',
|
|
174
|
-
|
|
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 ^',
|
|
175
240
|
' --user-data-dir="%OPENCLAW_CHROME_PROFILE_DIR%" ^',
|
|
176
241
|
' --profile-directory=Default ^',
|
|
177
242
|
' --no-first-run ^',
|
|
@@ -211,7 +276,8 @@ async function connectPreferredChrome() {
|
|
|
211
276
|
'fi',
|
|
212
277
|
'',
|
|
213
278
|
'# Chrome 136+ refuses --remote-debugging-port on the default profile directory, so run a',
|
|
214
|
-
'# 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.',
|
|
215
281
|
': "${OPENCLAW_CHROME_PROFILE_DIR:=$DEFAULT_DEBUG_PROFILE}"',
|
|
216
282
|
'',
|
|
217
283
|
'echo "Using: $CHROME_BIN"',
|
|
@@ -219,21 +285,28 @@ async function connectPreferredChrome() {
|
|
|
219
285
|
'pkill -f -- "--remote-debugging-port=9222" 2>/dev/null || true',
|
|
220
286
|
'sleep 2',
|
|
221
287
|
'',
|
|
222
|
-
'if [ ! -d "$OPENCLAW_CHROME_PROFILE_DIR/Default" ]
|
|
223
|
-
'
|
|
224
|
-
'
|
|
225
|
-
'
|
|
226
|
-
'
|
|
227
|
-
|
|
228
|
-
'
|
|
229
|
-
'
|
|
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',
|
|
230
303
|
'fi',
|
|
231
304
|
'mkdir -p "$OPENCLAW_CHROME_PROFILE_DIR"',
|
|
232
305
|
'',
|
|
233
306
|
'echo "Starting Chrome (profile: $OPENCLAW_CHROME_PROFILE_DIR)..."',
|
|
234
307
|
'"$CHROME_BIN" \\',
|
|
235
308
|
' --remote-debugging-port=9222 \\',
|
|
236
|
-
' --remote-
|
|
309
|
+
' --remote-debugging-address=127.0.0.1 \\',
|
|
237
310
|
' --user-data-dir="$OPENCLAW_CHROME_PROFILE_DIR" \\',
|
|
238
311
|
' --profile-directory=Default \\',
|
|
239
312
|
' --no-first-run \\',
|
|
@@ -300,9 +373,10 @@ async function connectPreferredChrome() {
|
|
|
300
373
|
|
|
301
374
|
// Patch the plugin's own copies too: the plugin re-syncs its skill folder into every
|
|
302
375
|
// workspace on startup, so an unpatched source would undo the profile change below.
|
|
376
|
+
const sourceScriptNames = ['start-chrome-debug.bat', 'start-chrome-debug.sh'];
|
|
303
377
|
let scriptsPatched = 0;
|
|
304
378
|
for (const dir of extensionDirs) {
|
|
305
|
-
for (const name of
|
|
379
|
+
for (const name of sourceScriptNames) {
|
|
306
380
|
const file = join(dir, name);
|
|
307
381
|
if (!existsSync(file)) continue;
|
|
308
382
|
const content = await fsp.readFile(file, 'utf8');
|
|
@@ -314,7 +388,22 @@ async function connectPreferredChrome() {
|
|
|
314
388
|
}
|
|
315
389
|
}
|
|
316
390
|
if (scriptsPatched > 0) {
|
|
317
|
-
sendLog(`[browser] Patched ${scriptsPatched} start-chrome script(s) to use
|
|
391
|
+
sendLog(`[browser] Patched ${scriptsPatched} start-chrome script(s) to use a dedicated Chrome profile seeded from yours (set OPENCLAW_CHROME_PROFILE_DIR to override).`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Some Docker projects mount `.openclaw/extensions` as a named volume, so the plugin's own
|
|
395
|
+
// files exist only inside the container. Every host path above then quietly finds nothing,
|
|
396
|
+
// the plugin re-syncs its stale script into the workspaces on each boot, and the operator
|
|
397
|
+
// keeps running the version that cannot open the debug port. Push the script in through the
|
|
398
|
+
// container instead.
|
|
399
|
+
if (!extensionDirs.some((dir) => existsSync(join(dir, sourceScriptNames[0])) || existsSync(join(dir, sourceScriptNames[1])))) {
|
|
400
|
+
const pushed = await pushBrowserScriptsIntoContainer(projectDir, aliases, [
|
|
401
|
+
['start-chrome-debug.bat', startChromeBat, '644'],
|
|
402
|
+
['start-chrome-debug.sh', startChromeSh, '755'],
|
|
403
|
+
], sendLog);
|
|
404
|
+
if (pushed > 0) {
|
|
405
|
+
sendLog(`[browser] Extensions live in a Docker volume — pushed ${pushed} start-chrome script(s) into the container so the plugin delivers the current one.`);
|
|
406
|
+
}
|
|
318
407
|
}
|
|
319
408
|
|
|
320
409
|
const browserMd = `# Browser Automation
|
|
@@ -366,12 +455,10 @@ Do not call \`search-tool.js\`; browser-automation does not own search. Use \`we
|
|
|
366
455
|
const hostOs = normalizeHostOs(await resolveProjectHostOs(projectDir));
|
|
367
456
|
const shouldKeepBat = hostOs === 'win';
|
|
368
457
|
// Shipped as start-chrome-debug.* by the plugin; delivered to the workspace as
|
|
369
|
-
// start-chrome.* — it launches
|
|
370
|
-
//
|
|
371
|
-
const sourceName = shouldKeepBat ? 'start-chrome-debug.bat' : 'start-chrome-debug.sh';
|
|
458
|
+
// start-chrome.* — it launches Chrome with the debug port open, so "debug" in the name only
|
|
459
|
+
// ever made people think it was a developer-only thing.
|
|
372
460
|
const scriptToKeep = shouldKeepBat ? 'start-chrome.bat' : 'start-chrome.sh';
|
|
373
461
|
const legacyScripts = ['start-chrome-debug.bat', 'start-chrome-debug.sh', shouldKeepBat ? 'start-chrome.sh' : 'start-chrome.bat'];
|
|
374
|
-
const sourceScript = extensionDirs.map((dir) => join(dir, sourceName)).find((file) => existsSync(file));
|
|
375
462
|
const sourceBrowserTool = extensionDirs.map((dir) => join(dir, 'browser-tool.js')).find((file) => existsSync(file));
|
|
376
463
|
|
|
377
464
|
let sanitized = 0;
|
|
@@ -396,16 +483,12 @@ Do not call \`search-tool.js\`; browser-automation does not own search. Use \`we
|
|
|
396
483
|
if (next !== content) await fsp.writeFile(targetBrowserTool, next, 'utf8');
|
|
397
484
|
}
|
|
398
485
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (nextScript !== scriptContent) await fsp.writeFile(targetScript, nextScript, 'utf8');
|
|
406
|
-
}
|
|
407
|
-
if (scriptToKeep.endsWith('.sh')) await fsp.chmod(targetScript, 0o755).catch(() => {});
|
|
408
|
-
}
|
|
486
|
+
// Written from the generator, not copied from the plugin: on a project whose extensions
|
|
487
|
+
// folder is a Docker volume there is no host copy to read, and the workspace would be left
|
|
488
|
+
// without a starter at all.
|
|
489
|
+
const targetScript = join(pluginSkillPath, scriptToKeep);
|
|
490
|
+
await fsp.writeFile(targetScript, scriptToKeep.endsWith('.bat') ? startChromeBat : startChromeSh, 'utf8');
|
|
491
|
+
if (scriptToKeep.endsWith('.sh')) await fsp.chmod(targetScript, 0o755).catch(() => {});
|
|
409
492
|
await fsp.writeFile(join(pluginSkillPath, 'BROWSER.md'), browserMd, 'utf8');
|
|
410
493
|
for (const dirName of ['cl-stealth-search', 'openclaw-smart-search']) {
|
|
411
494
|
await fsp.rm(join(workspacePath, 'plugin-skills', dirName), { recursive: true, force: true }).catch(() => {});
|
|
@@ -637,17 +720,58 @@ function run(cmd, args, opts = {}) {
|
|
|
637
720
|
});
|
|
638
721
|
}
|
|
639
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
|
+
|
|
640
750
|
async function getCurrentRuntimeVersions() {
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
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 || '',
|
|
650
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;
|
|
651
775
|
}
|
|
652
776
|
|
|
653
777
|
// Per-project cache for EXPENSIVE runtime/version probes (docker exec + openclaw CLI). These
|
|
@@ -663,8 +787,35 @@ function probeCacheGet(key) {
|
|
|
663
787
|
}
|
|
664
788
|
function probeCacheSet(key, value, ttlMs = 0) { _probeCache.set(key, { value, exp: ttlMs ? Date.now() + ttlMs : 0 }); }
|
|
665
789
|
function probeCacheClear(prefix = '') {
|
|
666
|
-
if (!prefix) { _probeCache.clear(); return; }
|
|
790
|
+
if (!prefix) { _probeCache.clear(); _probeInflight.clear(); return; }
|
|
667
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;
|
|
668
819
|
}
|
|
669
820
|
|
|
670
821
|
async function resolveProjectRuntimeVersions(projectDir, mode = state.mode || 'docker') {
|
|
@@ -2010,6 +2161,8 @@ async function waitForNativeGatewayZaloReady(projectDir, timeoutMs = 90000, chan
|
|
|
2010
2161
|
}
|
|
2011
2162
|
|
|
2012
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 || ''}`);
|
|
2013
2166
|
const cfgPath = join(projectDir, ".openclaw", "openclaw.json");
|
|
2014
2167
|
if (!existsSync(cfgPath)) throw httpError(404, "openclaw.json not found");
|
|
2015
2168
|
const cfg = JSON.parse(await fsp.readFile(cfgPath, "utf8"));
|
|
@@ -2268,7 +2421,17 @@ function buildZaloHealthSnapshot(cfg = {}, statusJson = null, credentialNames =
|
|
|
2268
2421
|
// ── Zalo health snapshot for the dashboard ──────────────────────────────────────
|
|
2269
2422
|
// Runtime JSON is authoritative and account-aware. Text parsing remains only as a
|
|
2270
2423
|
// compatibility fallback for older OpenClaw builds.
|
|
2271
|
-
|
|
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) {
|
|
2272
2435
|
const meta = {
|
|
2273
2436
|
supportedVersion: ZALO_CONNECT_VERSION,
|
|
2274
2437
|
installedVersion: null,
|
|
@@ -2285,19 +2448,24 @@ async function getZaloHealth(projectDir) {
|
|
|
2285
2448
|
if (meta.zaloModVersion) meta.zaloModInstalled = true;
|
|
2286
2449
|
|
|
2287
2450
|
const botContainer = getBotContainerName(projectDir);
|
|
2451
|
+
const native = isNativeProject(projectDir);
|
|
2288
2452
|
if (cfg.channels?.['zalo-connect']?.enabled) {
|
|
2289
2453
|
const manifestHost = join(projectDir, '.openclaw', 'extensions', 'zalo-connect', 'openclaw.plugin.json');
|
|
2290
2454
|
try {
|
|
2291
2455
|
meta.installedVersion = JSON.parse(await fsp.readFile(manifestHost, 'utf8')).version || null;
|
|
2292
2456
|
} catch {
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
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
|
+
}
|
|
2297
2466
|
}
|
|
2298
2467
|
}
|
|
2299
2468
|
|
|
2300
|
-
const native = isNativeProject(projectDir);
|
|
2301
2469
|
let containerRunning = false;
|
|
2302
2470
|
let statusJson = null;
|
|
2303
2471
|
let textStatus = '';
|
|
@@ -2307,7 +2475,9 @@ async function getZaloHealth(projectDir) {
|
|
|
2307
2475
|
// /health over loopback, then read channel status + credentials directly on the host.
|
|
2308
2476
|
const nmeta = readNativeMeta(projectDir) || {};
|
|
2309
2477
|
const port = String(nmeta.gatewayPort || state.gatewayPort || NATIVE_DEFAULT_GATEWAY_PORT);
|
|
2310
|
-
|
|
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);
|
|
2311
2481
|
if (containerRunning) {
|
|
2312
2482
|
try {
|
|
2313
2483
|
const r = await ocCapture(projectDir, ['channels', 'status', '--json'], { timeout: 20000 });
|
|
@@ -3764,7 +3934,8 @@ async function ensureChromeRelay() {
|
|
|
3764
3934
|
// Launch real host Chrome in remote-debugging mode (port 9222) so the browser-automation plugin
|
|
3765
3935
|
// can drive the user's actual Chrome (logged-in profile) instead of headless Chromium. The bot
|
|
3766
3936
|
// reaches it via CDP (host.docker.internal:9222 from the container). Detached: keeps running after
|
|
3767
|
-
// 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.
|
|
3768
3939
|
// On a headless VPS there is no Chrome to open here — instead we start the bridge relay and hand
|
|
3769
3940
|
// back copy-paste commands so the user runs Chrome on THEIR machine + a reverse SSH tunnel.
|
|
3770
3941
|
// Where Chrome keeps the operator's own profile, per OS. Chrome must not already be running
|
|
@@ -3847,8 +4018,10 @@ async function startChromeDebug() {
|
|
|
3847
4018
|
ok: true,
|
|
3848
4019
|
headless: true,
|
|
3849
4020
|
port: 9222,
|
|
3850
|
-
|
|
3851
|
-
|
|
4021
|
+
// Same dedicated profile directories the local button and the generated scripts use, so
|
|
4022
|
+
// an operator who has already run one of those keeps the session they signed in with.
|
|
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`,
|
|
3852
4025
|
tunnelCmd: `ssh -N -R 9222:127.0.0.1:9222 ${user}@${ip}`,
|
|
3853
4026
|
};
|
|
3854
4027
|
}
|
|
@@ -3867,12 +4040,20 @@ async function startChromeDebug() {
|
|
|
3867
4040
|
// --enable-automation, so navigator.webdriver stays false and there is no banner.
|
|
3868
4041
|
// Set OPENCLAW_CHROME_PROFILE_DIR to point somewhere else (anything but the default profile).
|
|
3869
4042
|
const userDataDir = process.env.OPENCLAW_CHROME_PROFILE_DIR || debugChromeProfileDir();
|
|
3870
|
-
|
|
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
|
+
}
|
|
3871
4050
|
const args = [
|
|
3872
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',
|
|
3873
4055
|
`--user-data-dir=${userDataDir}`,
|
|
3874
4056
|
'--profile-directory=Default',
|
|
3875
|
-
'--remote-allow-origins=*',
|
|
3876
4057
|
'--no-first-run',
|
|
3877
4058
|
'--no-default-browser-check',
|
|
3878
4059
|
];
|
|
@@ -4090,6 +4271,9 @@ async function listMarkdownFiles(projectDir, agentId = '') {
|
|
|
4090
4271
|
}
|
|
4091
4272
|
|
|
4092
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:');
|
|
4093
4277
|
const file = join(rootProjectDir, STATE_FILE);
|
|
4094
4278
|
await fsp.writeFile(file, JSON.stringify({
|
|
4095
4279
|
projectDir: state.projectDir,
|
|
@@ -4171,6 +4355,9 @@ function isRestrictedSystemDir(dirPath) {
|
|
|
4171
4355
|
// so a fresh `npx github:…` run (e.g. on a VPS where bots are already running) targets the
|
|
4172
4356
|
// live project instead of defaulting to an empty ~/openclaw-setup folder.
|
|
4173
4357
|
async function discoverDockerBotProjectRoots() {
|
|
4358
|
+
const ck = 'dockerroots';
|
|
4359
|
+
const cached = probeCacheGet(ck);
|
|
4360
|
+
if (cached) return cached;
|
|
4174
4361
|
const roots = [];
|
|
4175
4362
|
try {
|
|
4176
4363
|
const r = await runCapture(
|
|
@@ -4189,7 +4376,9 @@ async function discoverDockerBotProjectRoots() {
|
|
|
4189
4376
|
}
|
|
4190
4377
|
}
|
|
4191
4378
|
} catch {}
|
|
4192
|
-
|
|
4379
|
+
const unique = [...new Set(roots)];
|
|
4380
|
+
probeCacheSet(ck, unique, 15000);
|
|
4381
|
+
return unique;
|
|
4193
4382
|
}
|
|
4194
4383
|
|
|
4195
4384
|
// Native installs have no container to inspect, so we can't detect them the way Docker bots are
|
|
@@ -4288,7 +4477,15 @@ async function ensureProjectsLoaded(rootProjectDir) {
|
|
|
4288
4477
|
}
|
|
4289
4478
|
}
|
|
4290
4479
|
|
|
4291
|
-
|
|
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) {
|
|
4292
4489
|
await ensureProjectsLoaded(rootProjectDir);
|
|
4293
4490
|
|
|
4294
4491
|
// Surface projects whose bot is running in Docker even if this install has no saved
|
|
@@ -4316,16 +4513,14 @@ async function discoverProjects(rootProjectDir) {
|
|
|
4316
4513
|
}
|
|
4317
4514
|
}
|
|
4318
4515
|
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
}
|
|
4328
|
-
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);
|
|
4329
4524
|
|
|
4330
4525
|
state.projects.sort((a, b) => {
|
|
4331
4526
|
const aActive = state.projectDir && resolve(state.projectDir) === resolve(a.projectDir);
|
|
@@ -4914,6 +5109,11 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
4914
5109
|
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
4915
5110
|
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
4916
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(),
|
|
4917
5117
|
});
|
|
4918
5118
|
cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
|
|
4919
5119
|
cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:web']));
|
|
@@ -5013,6 +5213,11 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
5013
5213
|
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
5014
5214
|
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
5015
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(),
|
|
5016
5221
|
});
|
|
5017
5222
|
cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
|
|
5018
5223
|
cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:web']));
|
|
@@ -5868,6 +6073,15 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
|
|
|
5868
6073
|
// Bring the host-control service back up when the operator left it enabled, so the bot's
|
|
5869
6074
|
// saved instructions keep working across installer restarts.
|
|
5870
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(() => {});
|
|
5871
6085
|
}
|
|
5872
6086
|
|
|
5873
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/dist/web/app.js
CHANGED
|
@@ -623,7 +623,7 @@ function wireSkillsHandlers(scope = document) {
|
|
|
623
623
|
scope.querySelectorAll('[data-chrome-debug]').forEach(btn => btn.onclick = async () => {
|
|
624
624
|
btn.disabled = true;
|
|
625
625
|
try {
|
|
626
|
-
const r = await api('/api/browser/start-chrome
|
|
626
|
+
const r = await api('/api/browser/start-chrome', { method: 'POST', body: {} });
|
|
627
627
|
if (r.headless) {
|
|
628
628
|
// VPS has no display — guide the user to run Chrome on THEIR machine + a reverse tunnel.
|
|
629
629
|
state.confirmModal = {
|
|
@@ -639,8 +639,10 @@ function wireSkillsHandlers(scope = document) {
|
|
|
639
639
|
};
|
|
640
640
|
render();
|
|
641
641
|
} else {
|
|
642
|
-
|
|
643
|
-
|
|
642
|
+
// The profile is a copy of the operator's own, so their normal Chrome can stay open —
|
|
643
|
+
// say so, because the old flow required closing every Chrome window first.
|
|
644
|
+
showToast(t('Đã mở Chrome cho bot', 'Chrome opened for the bot'),
|
|
645
|
+
`${t('Cổng', 'Port')} ${r.port} — ${t('cửa sổ Chrome mới này dùng bản sao profile của bạn (đã đăng nhập sẵn); Chrome thường của bạn cứ để nguyên.', 'the new window runs a copy of your profile (already signed in); your normal Chrome can stay open.')}`, 'success');
|
|
644
646
|
}
|
|
645
647
|
} catch (err) {
|
|
646
648
|
showToast(t('Thất bại', 'Failed'), err.message, 'error');
|
|
@@ -1166,7 +1168,7 @@ function botSkillsPanel() {
|
|
|
1166
1168
|
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>`);
|
|
1167
1169
|
}
|
|
1168
1170
|
if (item.id === 'browser-automation' || item.id === 'openclaw-browser-automation') {
|
|
1169
|
-
secs.push(`<button class="secondary icon-btn2" type="button" data-chrome-debug title="${t('Mở Chrome
|
|
1171
|
+
secs.push(`<button class="secondary icon-btn2" type="button" data-chrome-debug title="${t('Mở Chrome trên máy này cho bot dùng. Chạy bản sao profile của bạn nên vẫn đăng nhập sẵn, và Chrome thường của bạn không bị đóng.','Open Chrome on this machine for the bot. It runs a copy of your profile, so it stays signed in, and your normal Chrome is left alone.')}" 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 cho bot','Open Chrome for bot')}</span></button>`);
|
|
1170
1172
|
}
|
|
1171
1173
|
} else {
|
|
1172
1174
|
secs.push(`<button class="secondary icon-btn2" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''}>${actionIcon('download')} ${ui('installVerb')}</button>`);
|