create-openclaw-bot 5.16.8 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/README.vi.md +8 -8
- package/dist/server/local-server.js +560 -13
- package/dist/setup/shared/bot-config-gen.js +17 -0
- package/dist/setup/shared/docker-gen.js +1 -1
- package/dist/setup/shared/windows-launcher-gen.js +199 -0
- package/dist/web/app.js +11 -6
- package/dist/web/styles.css +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
### One **Web UI** to scaffold, deploy & run free multi-bot AI assistants — zero terminal required
|
|
6
6
|
|
|
7
|
-
*Run one command → open the dashboard → your bot is live. Windows
|
|
7
|
+
*Run one command → open the dashboard → your bot is live. Windows, macOS, Linux, VPS — Docker-powered, auto-installed for you.*
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.17.0-0EA5E9?style=for-the-badge" alt="Version 5.17.0" /></a>
|
|
11
11
|
<a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
|
|
12
12
|
<a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
|
|
13
13
|
<a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
## 🆕 What's New in v5.
|
|
40
|
+
## 🆕 What's New in v5.17.0
|
|
41
41
|
|
|
42
42
|
- 🩹 **Native installs on OpenClaw 2026.8 are smooth from the first run** - this wave patches the remaining chain of issues measured on fresh customer machines: generated configs pass the new strict checks 100%, install steps run in the right order, and restarts/plugin installs no longer get blocked midway.
|
|
43
43
|
- 🔑 **9Router login works on Docker**: the new 9Router blocks the default password for "remote" access - and inside Docker every request counts as remote, so 123456 could never log in. It works now (still change the password once you're in).
|
package/README.vi.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
### Một **giao diện Web** để tạo, triển khai & vận hành trợ lý AI đa bot miễn phí — không cần gõ terminal
|
|
6
6
|
|
|
7
|
-
*Chạy một lệnh
|
|
7
|
+
*Chạy một lệnh, mở dashboard, bot lên sóng. Windows, macOS, Linux, VPS. Bot chạy thẳng trên máy, không cần Docker.*
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.17.0-0EA5E9?style=for-the-badge" alt="Version 5.17.0" /></a>
|
|
11
11
|
<a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
|
|
12
12
|
<a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
|
|
13
13
|
<a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
## 🆕 Có gì mới trong v5.
|
|
40
|
+
## 🆕 Có gì mới trong v5.17.0
|
|
41
41
|
|
|
42
42
|
- 🩹 **Cài native trên OpenClaw 2026.8 mượt từ phát đầu** - đợt này vá nốt chuỗi lỗi đo được khi dựng máy khách mới: cấu hình sinh ra sạch 100% với bộ soát khắt khe của OpenClaw mới, các bước cài tự chạy đúng thứ tự, và mọi thao tác khởi động lại/cài plugin không còn bị chặn giữa chừng.
|
|
43
43
|
- 🔑 **Đăng nhập 9Router hoạt động trên Docker**: bản 9Router mới chặn mật khẩu mặc định với truy cập "từ xa" - mà trong Docker thì mọi truy cập đều bị tính là từ xa, nên trước đây không cách nào vào được bằng 123456. Giờ đăng nhập bình thường (vào rồi vẫn nên đổi mật khẩu).
|
|
@@ -246,9 +246,10 @@ Bấm nút **Cập nhật** ở **góc trên bên phải giao diện Setup**. H
|
|
|
246
246
|
|
|
247
247
|
## 📋 Yêu cầu hệ thống
|
|
248
248
|
|
|
249
|
-
- **Node.js 24 LTS** (bắt buộc)
|
|
249
|
+
- **Node.js 24 LTS** (bắt buộc). Cả trình cài đặt lẫn bot đều chạy bằng Node. [Tải Node.js](https://nodejs.org/).
|
|
250
|
+
Máy chưa có Node thì trình cài đặt tự cài giúp bạn.
|
|
250
251
|
- **Git**: Đã cài đặt và có trong biến môi trường PATH.
|
|
251
|
-
- **Docker
|
|
252
|
+
- **Không cần Docker nữa.** Từ bản 5.17.0 bot chạy thẳng trên máy.
|
|
252
253
|
|
|
253
254
|
---
|
|
254
255
|
|
|
@@ -258,9 +259,8 @@ Bấm nút **Cập nhật** ở **góc trên bên phải giao diện Setup**. H
|
|
|
258
259
|
|
|
259
260
|
**1. Mở giao diện Setup** — chạy lệnh cài ở trên, dashboard sẽ mở trên trình duyệt.
|
|
260
261
|
|
|
261
|
-
**2. Chọn Hệ điều hành
|
|
262
|
-
|
|
263
|
-
- **Native** — nhẹ hơn, chạy bot thẳng trên máy (không cần Docker).
|
|
262
|
+
**2. Chọn Hệ điều hành** rồi bấm tiếp. Chế độ chạy giờ chỉ còn **Native**, tức bot chạy thẳng trên máy bạn.
|
|
263
|
+
Ô Docker vẫn hiện nhưng đã khoá, bấm không được. Lý do ghi ngay trên màn hình.
|
|
264
264
|
|
|
265
265
|
**3. Nhập đường dẫn & tên project** — nhập đường dẫn thư mục và tên project (ví dụ tên: `bot`), rồi bấm **Cài đặt**. Ví dụ đường dẫn:
|
|
266
266
|
- Windows: `D:\bot`
|
|
@@ -17,6 +17,7 @@ const { buildWorkspaceFileMap, buildCronjobSkillMd, buildInfographicGeneratorSki
|
|
|
17
17
|
const { buildOpenclawJson, buildEnvFileContent, buildExecApprovalsJson, buildZaloConnectChannelConfig } = loadSharedModule('../setup/shared/bot-config-gen.js', '__openclawBotConfig');
|
|
18
18
|
const { buildDockerArtifacts, contextDefaultsScript } = loadSharedModule('../setup/shared/docker-gen.js', '__openclawDockerGen');
|
|
19
19
|
const { HOST_UI_PS1, HOST_UI_PS1_VERSION } = loadSharedModule('../setup/shared/host-ui-ps1.js', '__openclawHostUiPs1');
|
|
20
|
+
const { buildWindowsLaunchers, WINDOWS_DESKTOP_LAUNCHERS } = loadSharedModule('../setup/shared/windows-launcher-gen.js', '__openclawWindowsLaunchers');
|
|
20
21
|
const { OPENCLAW_NPM_SPEC, NINE_ROUTER_NPM_SPEC, ZALO_CHANNEL_ID, ZALO_PLUGIN_ID, ZALO_CONNECT_VERSION, ZALO_CONNECT_PLUGIN_SPEC, build9RouterProviderConfig, get9RouterBaseUrl } = loadSharedModule('../setup/shared/common-gen.js', '__openclawCommon');
|
|
21
22
|
const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
|
|
22
23
|
|
|
@@ -47,11 +48,26 @@ function specMajorMinor(spec) {
|
|
|
47
48
|
return m ? Number(m[1]) * 100 + Number(m[2]) : 0;
|
|
48
49
|
}
|
|
49
50
|
let hostOcMajorMinorCache = null;
|
|
51
|
+
/**
|
|
52
|
+
* Drop the cached host version. MUST be called right after `npm i -g openclaw@…` changes the
|
|
53
|
+
* global install, because the cache below survives the upgrade and every version-gated
|
|
54
|
+
* decision after it would still see the OLD runtime.
|
|
55
|
+
*/
|
|
56
|
+
function invalidateHostOpenclawVersion() {
|
|
57
|
+
hostOcMajorMinorCache = null;
|
|
58
|
+
}
|
|
50
59
|
async function hostOpenclawMajorMinor() {
|
|
51
60
|
// Cache only a REAL version: during bot creation this is first called before
|
|
52
61
|
// `npm i -g openclaw` has run, and caching that 0 forever made prepareNativeStateHome
|
|
53
62
|
// skip the 2026.8 state-home move — daemon install then failed with "non-default
|
|
54
63
|
// state dir" (measured on a fresh native install, 03/09/2026).
|
|
64
|
+
//
|
|
65
|
+
// A real-but-STALE version is just as bad and does not self-heal: on a host that already
|
|
66
|
+
// had an older openclaw (vps_mkt ran 2026.7.1-2), the first call caches 202607, the
|
|
67
|
+
// installer then upgrades the global package to 2026.9.x, and prepareNativeStateHome still
|
|
68
|
+
// reads 202607 → skips the state-home move → `daemon install` dies with "non-default state
|
|
69
|
+
// dir or config path" and the gateway never starts. Measured on vps_mkt, 07/09/2026.
|
|
70
|
+
// Hence invalidateHostOpenclawVersion() after every global install/upgrade.
|
|
55
71
|
if (hostOcMajorMinorCache) return hostOcMajorMinorCache;
|
|
56
72
|
const r = await runCapture('openclaw', ['--version'], { shell: false }).catch(() => null);
|
|
57
73
|
hostOcMajorMinorCache = specMajorMinor(r && (r.stdout || r.stderr));
|
|
@@ -1198,7 +1214,12 @@ async function detectRuntime(projectDir) {
|
|
|
1198
1214
|
routerPort,
|
|
1199
1215
|
gatewayUrl: `http://127.0.0.1:${gatewayPort}`,
|
|
1200
1216
|
routerUrl: `http://127.0.0.1:${routerPort}`,
|
|
1201
|
-
|
|
1217
|
+
// Ask projectDeployMode, don't re-derive from the compose file: a project migrated to native
|
|
1218
|
+
// KEEPS its docker/ folder on purpose (that is the way back), so "compose file exists" stops
|
|
1219
|
+
// meaning "runs on docker" the moment migration lands. Getting this wrong made the dashboard
|
|
1220
|
+
// report OpenClaw and 9Router OFFLINE on a machine whose native gateway was answering 200 —
|
|
1221
|
+
// it was probing the stopped containers. Seen on win_kha, 10/09/2026.
|
|
1222
|
+
mode: projectDeployMode(projectDir),
|
|
1202
1223
|
cliGatewayStatus,
|
|
1203
1224
|
syncSource,
|
|
1204
1225
|
};
|
|
@@ -2755,15 +2776,34 @@ async function computeZaloHealth(projectDir) {
|
|
|
2755
2776
|
textStatus = `${r.stdout || ''}\n${r.stderr || ''}`;
|
|
2756
2777
|
} catch {}
|
|
2757
2778
|
}
|
|
2758
|
-
//
|
|
2759
|
-
//
|
|
2779
|
+
// Where the Zalo session file lives depends on the plugin version, and getting this wrong
|
|
2780
|
+
// makes a perfectly logged-in bot show as "chưa đăng nhập":
|
|
2781
|
+
// <=3.1.4 <home>/.openclaw/zalo-connect-credentials[-<account>].json
|
|
2782
|
+
// >=3.1.5 <state>/credentials/zalo-connect/credentials[-<account>].json
|
|
2783
|
+
// 3.1.5 MOVES the old file on first run and leaves a *.migrated stub behind, so a scan that
|
|
2784
|
+
// only knows the old shape finds nothing at all. Look in the new layout first, then fall
|
|
2785
|
+
// back, and normalise the new names to the old ones so the caller's lookup keeps working.
|
|
2786
|
+
// Measured on win_kha after the zalo-connect upgrade, 10/09/2026.
|
|
2760
2787
|
const credRe = /^zalo-connect-credentials(?:-[^.]+)?\.json$/i;
|
|
2761
|
-
|
|
2788
|
+
const newCredRe = /^credentials(?:-[^.]+)?\.json$/i;
|
|
2789
|
+
const toLegacyName = (n) => n.replace(/^credentials/, 'zalo-connect-credentials');
|
|
2790
|
+
for (const dir of [
|
|
2791
|
+
join(projectDir, '.openclaw', 'credentials', 'zalo-connect'),
|
|
2792
|
+
join(os.homedir(), '.openclaw', 'credentials', 'zalo-connect'),
|
|
2793
|
+
]) {
|
|
2762
2794
|
try {
|
|
2763
|
-
const names = (await fsp.readdir(dir)).filter((n) =>
|
|
2764
|
-
if (names.length) { credentialNames = names; break; }
|
|
2795
|
+
const names = (await fsp.readdir(dir)).filter((n) => newCredRe.test(n));
|
|
2796
|
+
if (names.length) { credentialNames = names.map(toLegacyName); break; }
|
|
2765
2797
|
} catch {}
|
|
2766
2798
|
}
|
|
2799
|
+
if (!credentialNames || !credentialNames.length) {
|
|
2800
|
+
for (const dir of [join(projectDir, '.openclaw'), join(os.homedir(), '.openclaw')]) {
|
|
2801
|
+
try {
|
|
2802
|
+
const names = (await fsp.readdir(dir)).filter((n) => credRe.test(n));
|
|
2803
|
+
if (names.length) { credentialNames = names; break; }
|
|
2804
|
+
} catch {}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2767
2807
|
}
|
|
2768
2808
|
} else {
|
|
2769
2809
|
try {
|
|
@@ -3530,6 +3570,286 @@ async function prepareNativeStateHome(projectDir) {
|
|
|
3530
3570
|
return installEnv;
|
|
3531
3571
|
}
|
|
3532
3572
|
|
|
3573
|
+
/**
|
|
3574
|
+
* Turn OpenClaw's real computer-use on or off for a project.
|
|
3575
|
+
*
|
|
3576
|
+
* Three pieces must line up, and a bot with only two of them reports "I don't have permission"
|
|
3577
|
+
* without saying which one is missing — that ambiguity cost a whole debugging session:
|
|
3578
|
+
* 1. `computer` (+ `screen`) allowed in tools.alsoAllow — otherwise the agent never receives
|
|
3579
|
+
* the tool at all, no matter what else is configured;
|
|
3580
|
+
* 2. the `cua-computer` plugin, which is MANDATORY on Windows and ships disabled. It also has
|
|
3581
|
+
* to be on plugins.allow, or enabling is refused with "blocked by allowlist";
|
|
3582
|
+
* 3. a node host process advertising `computer.act` + `screen.snapshot`. The tool is only the
|
|
3583
|
+
* model-facing surface; the node is what actually touches the screen, which is why a
|
|
3584
|
+
* correctly configured tool still does nothing on its own.
|
|
3585
|
+
*
|
|
3586
|
+
* The node host must run inside a real interactive desktop session — started from a service or
|
|
3587
|
+
* an SSH session it either dies with the session or cannot see a desktop at all. Launching it
|
|
3588
|
+
* from here works because the operator pressing the button is sitting at that desktop.
|
|
3589
|
+
*/
|
|
3590
|
+
async function setComputerUse(projectDir, enable) {
|
|
3591
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
3592
|
+
if (!existsSync(cfgPath)) return { ok: false, error: 'openclaw.json not found' };
|
|
3593
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
3594
|
+
|
|
3595
|
+
cfg.tools = (cfg.tools && typeof cfg.tools === 'object') ? cfg.tools : {};
|
|
3596
|
+
const allow = new Set(Array.isArray(cfg.tools.alsoAllow) ? cfg.tools.alsoAllow : []);
|
|
3597
|
+
cfg.plugins = (cfg.plugins && typeof cfg.plugins === 'object') ? cfg.plugins : {};
|
|
3598
|
+
cfg.plugins.allow = Array.isArray(cfg.plugins.allow) ? cfg.plugins.allow : [];
|
|
3599
|
+
cfg.plugins.entries = (cfg.plugins.entries && typeof cfg.plugins.entries === 'object') ? cfg.plugins.entries : {};
|
|
3600
|
+
|
|
3601
|
+
if (enable) {
|
|
3602
|
+
allow.add('computer');
|
|
3603
|
+
allow.add('screen');
|
|
3604
|
+
if (!cfg.plugins.allow.includes('cua-computer')) cfg.plugins.allow.push('cua-computer');
|
|
3605
|
+
cfg.plugins.entries['cua-computer'] = { ...(cfg.plugins.entries['cua-computer'] || {}), enabled: true };
|
|
3606
|
+
} else {
|
|
3607
|
+
allow.delete('computer');
|
|
3608
|
+
allow.delete('screen');
|
|
3609
|
+
if (cfg.plugins.entries['cua-computer']) cfg.plugins.entries['cua-computer'].enabled = false;
|
|
3610
|
+
}
|
|
3611
|
+
cfg.tools.alsoAllow = [...allow];
|
|
3612
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
3613
|
+
|
|
3614
|
+
if (!enable) {
|
|
3615
|
+
await stopNodeHost().catch(() => {});
|
|
3616
|
+
sendLog('[computer-use] Đã tắt: gỡ tool computer/screen và dừng node điều khiển.');
|
|
3617
|
+
return { ok: true, enabled: false };
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
sendLog('[computer-use] Đã bật tool computer + plugin cua-computer. Đang khởi động node điều khiển...');
|
|
3621
|
+
const meta = readNativeMeta(projectDir) || {};
|
|
3622
|
+
const port = meta.gatewayPort || state.gatewayPort || NATIVE_DEFAULT_GATEWAY_PORT;
|
|
3623
|
+
await startNodeHost(projectDir, port);
|
|
3624
|
+
return { ok: true, enabled: true, gatewayPort: port };
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
/** Run `openclaw node run` detached so it outlives this request but stays in this desktop session. */
|
|
3628
|
+
async function startNodeHost(projectDir, gatewayPort) {
|
|
3629
|
+
if (await nodeHostRunning()) { sendLog('[computer-use] Node điều khiển đã chạy sẵn.'); return; }
|
|
3630
|
+
const a = ocArgv(projectDir, ['node', 'run', '--host', '127.0.0.1', '--port', String(gatewayPort)]);
|
|
3631
|
+
const child = spawn(a.cmd, a.args, { ...a.opts, detached: true, stdio: 'ignore', windowsHide: true });
|
|
3632
|
+
child.on('error', (err) => sendLog(`[computer-use] không chạy được node host: ${err.message}`));
|
|
3633
|
+
child.unref();
|
|
3634
|
+
// Confirm instead of assuming: a node host that failed to start looks exactly like one that
|
|
3635
|
+
// started, until the bot says it has no permission.
|
|
3636
|
+
for (let i = 0; i < 15; i++) {
|
|
3637
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
3638
|
+
if (await nodeHostRunning()) { sendLog('[computer-use] Node điều khiển đã kết nối.'); return; }
|
|
3639
|
+
}
|
|
3640
|
+
sendLog('[computer-use] Node điều khiển chưa lên sau 30s — kiểm tra lại bằng `openclaw node status`.');
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
async function nodeHostRunning() {
|
|
3644
|
+
if (process.platform === 'win32') {
|
|
3645
|
+
const r = await runCapture('powershell', ['-NoProfile', '-Command',
|
|
3646
|
+
"(Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | Where-Object { $_.CommandLine -like '*node*run*--host*' } | Measure-Object).Count"], { shell: false, timeout: 8000 });
|
|
3647
|
+
return Number(String(r.stdout || '0').trim()) > 0;
|
|
3648
|
+
}
|
|
3649
|
+
const r = await runCapture('sh', ['-c', "ps ax | grep -c '[n]ode run --host'"], { shell: false, timeout: 8000 });
|
|
3650
|
+
return Number(String(r.stdout || '0').trim()) > 0;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
async function stopNodeHost() {
|
|
3654
|
+
if (process.platform === 'win32') {
|
|
3655
|
+
await runCapture('powershell', ['-NoProfile', '-Command',
|
|
3656
|
+
"Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | Where-Object { $_.CommandLine -like '*node*run*--host*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -EA SilentlyContinue }"], { shell: false, timeout: 10000 });
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
await runCapture('sh', ['-c', "pkill -f 'node run --host' || true"], { shell: false, timeout: 10000 });
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
/**
|
|
3663
|
+
* Write the double-click launchers into the project and copy the operator-facing ones to the
|
|
3664
|
+
* Desktop. Safe to re-run: it overwrites, so a fixed launcher reaches machines on the next
|
|
3665
|
+
* install or update instead of leaving the customer pressing a stale copy.
|
|
3666
|
+
*/
|
|
3667
|
+
async function writeWindowsLaunchers(projectDir, gatewayPort, routerPort) {
|
|
3668
|
+
const files = buildWindowsLaunchers({ projectDir, gatewayPort, routerPort, setupPort: activeUiPort || 51789 });
|
|
3669
|
+
for (const [name, content] of Object.entries(files)) {
|
|
3670
|
+
await fsp.writeFile(join(projectDir, name), content, 'utf8');
|
|
3671
|
+
}
|
|
3672
|
+
const desktop = join(os.homedir(), 'Desktop');
|
|
3673
|
+
if (existsSync(desktop)) {
|
|
3674
|
+
for (const name of WINDOWS_DESKTOP_LAUNCHERS) {
|
|
3675
|
+
await fsp.writeFile(join(desktop, name), files[name], 'utf8').catch(() => {});
|
|
3676
|
+
}
|
|
3677
|
+
sendLog(`[native] Đã đặt ${WINDOWS_DESKTOP_LAUNCHERS.length} tệp bấm-đúp lên Desktop.`);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
/**
|
|
3682
|
+
* Move a running docker project onto native, keeping every bot and every workspace file.
|
|
3683
|
+
*
|
|
3684
|
+
* Why this exists: openclaw >=2026.9 writes files through @openclaw/fs-safe, which fstat()s the
|
|
3685
|
+
* file after an atomic rename. That check cannot pass through a Docker Desktop bind mount on
|
|
3686
|
+
* Windows, so the moment a docker image is rebuilt onto 2026.9.x the gateway can no longer write
|
|
3687
|
+
* its own config: every boot is unclean, the restart-loop breaker trips, and the bot is down with
|
|
3688
|
+
* a misleading "ENOENT: fstat". Measured end to end on win_kha, 09/09/2026 — native on the very
|
|
3689
|
+
* same NTFS path works, so the fault is the bind-mount layer, not the disk.
|
|
3690
|
+
*
|
|
3691
|
+
* The data lives in two different places and only one of them is visible on the host:
|
|
3692
|
+
* bind-mounted paths are already on disk, but named volumes are not — state (the sqlite DB with
|
|
3693
|
+
* every session), the plugin tree, and 9router's login live inside volumes. Copy those out FIRST;
|
|
3694
|
+
* everything after this point is reversible, the containers and image are left untouched.
|
|
3695
|
+
*/
|
|
3696
|
+
async function migrateDockerProjectToNative(projectDir, { osChoice = '' } = {}) {
|
|
3697
|
+
const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
|
|
3698
|
+
if (!existsSync(composeFile)) return false;
|
|
3699
|
+
sendLog('[migrate] Chuyển project từ Docker sang native...');
|
|
3700
|
+
|
|
3701
|
+
// 1. Stop the containers so nothing writes while we copy. Never `down -v` — that deletes the
|
|
3702
|
+
// very volumes holding the customer's sessions.
|
|
3703
|
+
await run('docker', ['compose', '-f', composeFile, 'stop']).catch(() => {});
|
|
3704
|
+
// Stopping is not enough: the containers carry a restart policy, so the next reboot brings them
|
|
3705
|
+
// back and they grab 18789/18790 before the native gateway can bind — the native process then
|
|
3706
|
+
// dies with EADDRINUSE seconds after reporting "ready", which reads like a random crash.
|
|
3707
|
+
// Measured on win_kha after its first reboot, 10/09/2026. Clear the policy, keep the containers.
|
|
3708
|
+
const psAll = await runCapture('docker', ['compose', '-f', composeFile, 'ps', '-a', '--format', '{{.Name}}'], { shell: false });
|
|
3709
|
+
const names = String(psAll.stdout || '').split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
|
|
3710
|
+
if (names.length) await run('docker', ['update', '--restart=no', ...names]).catch(() => {});
|
|
3711
|
+
|
|
3712
|
+
// 2. Copy every named volume out to the host path it maps to. Read the mapping from the
|
|
3713
|
+
// containers themselves rather than guessing volume names: compose prefixes them with the
|
|
3714
|
+
// project name, which differs per machine.
|
|
3715
|
+
const psOut = await runCapture('docker', ['compose', '-f', composeFile, 'ps', '-a', '--format', '{{.Name}}'], { shell: false });
|
|
3716
|
+
const containers = String(psOut.stdout || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
3717
|
+
const CONTAINER_HOME = '/home/node/project';
|
|
3718
|
+
let copied = 0;
|
|
3719
|
+
for (const name of containers) {
|
|
3720
|
+
const insp = await runCapture('docker', ['inspect', name, '--format', '{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Destination}}\n{{end}}'], { shell: false });
|
|
3721
|
+
for (const line of String(insp.stdout || '').split(/\r?\n/)) {
|
|
3722
|
+
const [type, volName, dest] = line.split('|').map((s) => (s || '').trim());
|
|
3723
|
+
if (type !== 'volume' || !volName || !dest) continue;
|
|
3724
|
+
// Map the in-container path back onto the project on disk.
|
|
3725
|
+
let hostDir = null;
|
|
3726
|
+
if (dest.startsWith(`${CONTAINER_HOME}/`)) hostDir = join(projectDir, dest.slice(CONTAINER_HOME.length + 1));
|
|
3727
|
+
else if (dest === '/root/.9router' || dest.endsWith('/.9router')) hostDir = join(projectDir, '.9router');
|
|
3728
|
+
if (!hostDir) { sendLog(`[migrate] bỏ qua volume ${volName} (${dest}) — không map được về project`); continue; }
|
|
3729
|
+
await fsp.mkdir(hostDir, { recursive: true });
|
|
3730
|
+
// Copy with a throwaway container: the volume has no host path we can read directly.
|
|
3731
|
+
const r = await runCapture('docker', [
|
|
3732
|
+
'run', '--rm', '-v', `${volName}:/__src:ro`, '-v', `${hostDir}:/__dst`,
|
|
3733
|
+
'node:22-slim', 'sh', '-c', 'cp -a /__src/. /__dst/ 2>/dev/null; ls -A /__dst | head -1',
|
|
3734
|
+
], { shell: false });
|
|
3735
|
+
if (r.stdout && r.stdout.trim()) { copied++; sendLog(`[migrate] ${volName} → ${hostDir}`); }
|
|
3736
|
+
else sendLog(`[migrate] ${volName} rỗng hoặc không chép được — kiểm tra tay: ${hostDir}`);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
sendLog(`[migrate] đã đưa ${copied} volume xuống đĩa.`);
|
|
3740
|
+
|
|
3741
|
+
// 3. Rewrite what only made sense inside the container.
|
|
3742
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
3743
|
+
if (existsSync(cfgPath)) {
|
|
3744
|
+
const before = await fsp.readFile(cfgPath, 'utf8');
|
|
3745
|
+
await fsp.writeFile(`${cfgPath}.bak-truoc-native`, before, 'utf8');
|
|
3746
|
+
let cfg = null;
|
|
3747
|
+
try { cfg = JSON.parse(before); } catch { cfg = null; }
|
|
3748
|
+
if (cfg) {
|
|
3749
|
+
// 3a. Container-absolute workspace paths. On Windows these become "C:\home\node\..." and
|
|
3750
|
+
// every plugin fails its install-dir check.
|
|
3751
|
+
const projPosix = join(projectDir, '.openclaw').replace(/\\/g, '/');
|
|
3752
|
+
let text = JSON.stringify(cfg, null, 2).split(`${CONTAINER_HOME}/.openclaw`).join(projPosix);
|
|
3753
|
+
cfg = JSON.parse(text);
|
|
3754
|
+
// 3a-ter. RELATIVE workspace paths. Inside the container the process always ran from
|
|
3755
|
+
// /home/node/project so ".openclaw/workspace-x" resolved correctly. Native has no such
|
|
3756
|
+
// guarantee: the customer double-clicks a launcher from wherever they filed it, and the
|
|
3757
|
+
// agent then looks for its workspace under THAT folder and dies with WORKSPACE_VANISHED
|
|
3758
|
+
// — while agents that happen to carry an absolute path keep working, so only some bots
|
|
3759
|
+
// break. Exactly what happened when the launchers were moved into a Desktop subfolder
|
|
3760
|
+
// (win_kha 10/09/2026). Pin every workspace to the project.
|
|
3761
|
+
{
|
|
3762
|
+
const ag = cfg.agents || {};
|
|
3763
|
+
const list = ag.entries && typeof ag.entries === 'object' ? Object.entries(ag.entries)
|
|
3764
|
+
: Array.isArray(ag.list) ? ag.list.map((a) => [a && a.id, a]) : [];
|
|
3765
|
+
for (const [aid, meta] of list) {
|
|
3766
|
+
if (!meta || typeof meta.workspace !== 'string') continue;
|
|
3767
|
+
const w = meta.workspace;
|
|
3768
|
+
if (/^[a-zA-Z]:[\\/]/.test(w) || w.startsWith('/')) continue; // already absolute
|
|
3769
|
+
meta.workspace = join(projectDir, w.replace(/^\.[\\/]/, '')).replace(/\\/g, '/');
|
|
3770
|
+
sendLog(`[migrate] workspace ${aid}: "${w}" → "${meta.workspace}" (đường tương đối phụ thuộc chỗ bấm tệp)`);
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
text = JSON.stringify(cfg, null, 2);
|
|
3774
|
+
|
|
3775
|
+
// 3a-bis. Docker-only HOSTNAMES. `http://9router:20128` resolves through compose's internal
|
|
3776
|
+
// DNS and nowhere else, so on native every model call dies with
|
|
3777
|
+
// `getaddrinfo ENOTFOUND 9router` — the bot still reacts (that is the channel, no model
|
|
3778
|
+
// needed) but can never reply, which reads as "bot ignores me". Same story for
|
|
3779
|
+
// host.docker.internal and the 192.168.65.x Desktop bridge. Measured win_kha 10/09/2026.
|
|
3780
|
+
text = text
|
|
3781
|
+
.split('http://9router:20128').join('http://127.0.0.1:20128')
|
|
3782
|
+
.split('http://host.docker.internal:20128').join('http://127.0.0.1:20128')
|
|
3783
|
+
.replace(/http:\/\/192\.168\.65\.\d+:(\d+)/g, 'http://127.0.0.1:$1');
|
|
3784
|
+
cfg = JSON.parse(text);
|
|
3785
|
+
// 3b. bind. Inside docker, 0.0.0.0 was safe because the port was published to loopback only.
|
|
3786
|
+
// Native binds the host directly, so the same value would expose the gateway to the LAN.
|
|
3787
|
+
cfg.gateway = (cfg.gateway && typeof cfg.gateway === 'object') ? cfg.gateway : {};
|
|
3788
|
+
if (cfg.gateway.bind !== 'loopback') {
|
|
3789
|
+
cfg.gateway.bind = 'loopback';
|
|
3790
|
+
delete cfg.gateway.customBindHost;
|
|
3791
|
+
if (cfg.gateway.controlUi) {
|
|
3792
|
+
cfg.gateway.controlUi.allowedOrigins = ['http://localhost:' + (cfg.gateway.port || NATIVE_DEFAULT_GATEWAY_PORT), 'http://127.0.0.1:' + (cfg.gateway.port || NATIVE_DEFAULT_GATEWAY_PORT)];
|
|
3793
|
+
}
|
|
3794
|
+
sendLog('[migrate] gateway.bind → loopback (native bind thẳng lên host, 0.0.0.0 là mở ra mạng)');
|
|
3795
|
+
}
|
|
3796
|
+
// 3c. The browser-automation plugin rewrites the browser block on every boot, so simply
|
|
3797
|
+
// deleting the bad key below is not enough — it grows back and the config is invalid
|
|
3798
|
+
// again by the next restart (same shape as the old toolResultMaxChars reinfection).
|
|
3799
|
+
// Take the pen away from it; patchDocker is pointless here now too.
|
|
3800
|
+
const baEntry = cfg.plugins && cfg.plugins.entries && cfg.plugins.entries['browser-automation'];
|
|
3801
|
+
if (baEntry && typeof baEntry === 'object') {
|
|
3802
|
+
baEntry.config = (baEntry.config && typeof baEntry.config === 'object') ? baEntry.config : {};
|
|
3803
|
+
baEntry.config.manageBrowserConfig = false;
|
|
3804
|
+
baEntry.config.patchDocker = false;
|
|
3805
|
+
sendLog('[migrate] tắt browser-automation.manageBrowserConfig (nó ghi lại khoá 2026.9 từ chối mỗi lần boot)');
|
|
3806
|
+
}
|
|
3807
|
+
// 3d. browser profile colour: required up to 2026.8, rejected outright by 2026.9.
|
|
3808
|
+
const profiles = (cfg.browser && cfg.browser.profiles) || null;
|
|
3809
|
+
if (profiles && typeof profiles === 'object') {
|
|
3810
|
+
for (const key of Object.keys(profiles)) {
|
|
3811
|
+
if (profiles[key] && typeof profiles[key] === 'object' && profiles[key].color !== undefined) {
|
|
3812
|
+
delete profiles[key].color;
|
|
3813
|
+
sendLog(`[migrate] gỡ browser.profiles.${key}.color (2026.9 không nhận)`);
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
// 4. Backup folders sitting inside extensions/ are still scanned as real plugins — that is where
|
|
3822
|
+
// the long-standing "duplicate plugin id" warnings came from. A leading dot does NOT hide them
|
|
3823
|
+
// on Windows, so move them out of the tree entirely.
|
|
3824
|
+
const extDir = join(projectDir, '.openclaw', 'extensions');
|
|
3825
|
+
if (existsSync(extDir)) {
|
|
3826
|
+
const parked = join(projectDir, '_plugin-backups');
|
|
3827
|
+
for (const entry of await fsp.readdir(extDir).catch(() => [])) {
|
|
3828
|
+
if (!/\.(bak|broken|old)[-.]|^\.openclaw-install-backups$/i.test(entry)) continue;
|
|
3829
|
+
await fsp.mkdir(parked, { recursive: true });
|
|
3830
|
+
await fsp.rename(join(extDir, entry), join(parked, entry.replace(/^\./, ''))).catch(() => {});
|
|
3831
|
+
sendLog(`[migrate] dời bản sao lưu plugin ra khỏi extensions/: ${entry}`);
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
// 5. zalo-connect wrote its Zalo sessions next to the container's HOME, which was itself
|
|
3836
|
+
// <project>/.openclaw — leaving them one level too deep. Lift them so 3.1.5's one-time
|
|
3837
|
+
// migration finds them; without this every account comes back "not authenticated".
|
|
3838
|
+
const nested = join(projectDir, '.openclaw', '.openclaw');
|
|
3839
|
+
if (existsSync(nested)) {
|
|
3840
|
+
for (const entry of await fsp.readdir(nested).catch(() => [])) {
|
|
3841
|
+
if (!/^zalo-connect-credentials.*\.json$/.test(entry)) continue;
|
|
3842
|
+
const target = join(projectDir, '.openclaw', entry);
|
|
3843
|
+
if (existsSync(target)) continue;
|
|
3844
|
+
await fsp.copyFile(join(nested, entry), target).catch(() => {});
|
|
3845
|
+
sendLog(`[migrate] nâng phiên Zalo lên đúng chỗ: ${entry}`);
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
sendLog('[migrate] Xong phần dữ liệu. Container và image giữ nguyên để còn quay lui.');
|
|
3850
|
+
return true;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3533
3853
|
/**
|
|
3534
3854
|
* Bring a native project up end to end: 9router → smart-route sync → resolve its API key into
|
|
3535
3855
|
* openclaw.json → install the gateway as a managed service. Order matters: the gateway must boot
|
|
@@ -3547,6 +3867,15 @@ async function startNativeRuntime({ projectDir, osChoice = '', gatewayPort, rout
|
|
|
3547
3867
|
'utf8',
|
|
3548
3868
|
);
|
|
3549
3869
|
|
|
3870
|
+
// Windows has no usable service story for this (see windows-launcher-gen.js): the Scheduled
|
|
3871
|
+
// Task openclaw installs is bound to a login session, so it dies with the SSH session and is
|
|
3872
|
+
// refused outright when set to run at boot. Give the owner double-click launchers instead —
|
|
3873
|
+
// the same shape as the .command files on macOS — and put the three they actually press on
|
|
3874
|
+
// the Desktop where they can find them.
|
|
3875
|
+
if (process.platform === 'win32') {
|
|
3876
|
+
await writeWindowsLaunchers(projectDir, gwPort, rtPort).catch((e) => sendLog(`[native] launcher: ${e.message}`));
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3550
3879
|
await startNative9Router(projectDir);
|
|
3551
3880
|
|
|
3552
3881
|
// Same smart-route sync the docker sidecar runs, pointed at the native DB path. Without it
|
|
@@ -3808,12 +4137,37 @@ async function updateRuntime(target, projectDir) {
|
|
|
3808
4137
|
if (isNativeProject(projectDir)) {
|
|
3809
4138
|
sendLog(`[native] Updating ${target} → ${spec}`);
|
|
3810
4139
|
await run('npm', ['install', '-g', spec]);
|
|
4140
|
+
if (!isRouter) invalidateHostOpenclawVersion();
|
|
3811
4141
|
if (isRouter) await startNative9Router(projectDir, { restart: true }).catch((e) => sendLog(`[native] 9router restart: ${e.message}`));
|
|
3812
4142
|
else await restartNativeRuntime(projectDir);
|
|
3813
4143
|
await syncRuntimeState(projectDir, { full: true }).catch(() => {});
|
|
3814
4144
|
probeCacheClear();
|
|
3815
4145
|
return { ok: true, target, spec, mode: 'native' };
|
|
3816
4146
|
}
|
|
4147
|
+
// A docker project asked to update is a docker project about to rebuild its image — which is
|
|
4148
|
+
// exactly the step that lands it on openclaw >=2026.9 and breaks writing through the bind mount
|
|
4149
|
+
// (see migrateDockerProjectToNative). So the update IS the migration: move the data onto native
|
|
4150
|
+
// first, then update the native way. Doing it here rather than on every page load keeps it a
|
|
4151
|
+
// deliberate act by the customer, not something that reshapes their machine behind their back.
|
|
4152
|
+
if (!isNativeProject(projectDir) && projectDir && existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'))) {
|
|
4153
|
+
sendLog('[update] Project đang chạy Docker — chuyển sang native trước khi cập nhật.');
|
|
4154
|
+
await ensureNodeInstalled();
|
|
4155
|
+
await migrateDockerProjectToNative(projectDir, { osChoice: state.os || '' });
|
|
4156
|
+
await run('npm', ['install', '-g', OPENCLAW_NPM_SPEC]);
|
|
4157
|
+
invalidateHostOpenclawVersion();
|
|
4158
|
+
await run('npm', ['install', '-g', NINE_ROUTER_NPM_SPEC]);
|
|
4159
|
+
const meta = readNativeMeta(projectDir) || {};
|
|
4160
|
+
await startNativeRuntime({
|
|
4161
|
+
projectDir,
|
|
4162
|
+
osChoice: state.os || '',
|
|
4163
|
+
gatewayPort: meta.gatewayPort || state.gatewayPort,
|
|
4164
|
+
routerPort: meta.routerPort || state.routerPort,
|
|
4165
|
+
});
|
|
4166
|
+
await syncRuntimeState(projectDir, { full: true }).catch(() => {});
|
|
4167
|
+
probeCacheClear();
|
|
4168
|
+
sendLog('[update] Đã chuyển sang native. Container Docker được giữ nguyên (đã dừng) để còn quay lui.');
|
|
4169
|
+
return { ok: true, target, spec, mode: 'native', migratedFromDocker: true };
|
|
4170
|
+
}
|
|
3817
4171
|
if (state.mode === 'docker' && projectDir) {
|
|
3818
4172
|
const dockerDir = join(projectDir, 'docker', 'openclaw');
|
|
3819
4173
|
if (isRouter) {
|
|
@@ -4127,11 +4481,63 @@ function detectHostApps() {
|
|
|
4127
4481
|
add('zalo', [join(local, 'Programs', 'Zalo', 'Zalo.exe'), join(local, 'Zalo', 'Zalo.exe')]);
|
|
4128
4482
|
add('explorer', ['C:\\Windows\\explorer.exe']);
|
|
4129
4483
|
add('notepad', ['C:\\Windows\\System32\\notepad.exe']);
|
|
4484
|
+
// A hand-written shortlist only covers what WE thought of. Every customer machine has its
|
|
4485
|
+
// own software, and the bot is useless the moment it is asked for something not on the list —
|
|
4486
|
+
// "mở TeamViewer" fails not because the app is missing but because nobody enumerated it.
|
|
4487
|
+
// The Start Menu is the one place Windows guarantees an entry per installed app, and a .lnk
|
|
4488
|
+
// launches correctly without knowing where the .exe actually lives. Scanning it turns the
|
|
4489
|
+
// list from "5 apps we guessed" into "everything this machine has", and it stays correct
|
|
4490
|
+
// when the customer installs something new. Measured on win_kha: 5 → 182 apps.
|
|
4491
|
+
Object.assign(apps, scanWindowsStartMenuApps(), apps); // hand-written entries win
|
|
4130
4492
|
} else if (process.platform === 'darwin') {
|
|
4131
4493
|
add('teamviewer', ['/Applications/TeamViewer.app']);
|
|
4132
4494
|
add('chrome', ['/Applications/Google Chrome.app']);
|
|
4133
4495
|
add('zalo', ['/Applications/Zalo.app']);
|
|
4134
4496
|
add('finder', ['/System/Library/CoreServices/Finder.app']);
|
|
4497
|
+
// Same idea as Windows: enumerate what is really installed instead of guessing.
|
|
4498
|
+
Object.assign(apps, scanMacApplications(), apps);
|
|
4499
|
+
}
|
|
4500
|
+
return apps;
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
/** Every .lnk under both Start Menu trees, keyed by a slug of its name. */
|
|
4504
|
+
function scanWindowsStartMenuApps() {
|
|
4505
|
+
const apps = {};
|
|
4506
|
+
// Uninstallers and doc links are not apps; opening one by accident is worse than not having it.
|
|
4507
|
+
const SKIP = /(uninstall|gỡ cài đặt|go cai dat|readme|help|documentation|website|release notes|license|repair|modify)/i;
|
|
4508
|
+
const roots = [
|
|
4509
|
+
join(process.env.ProgramData || 'C:\\ProgramData', 'Microsoft', 'Windows', 'Start Menu', 'Programs'),
|
|
4510
|
+
join(process.env.APPDATA || '', 'Microsoft', 'Windows', 'Start Menu', 'Programs'),
|
|
4511
|
+
];
|
|
4512
|
+
const walk = (dir, depth = 0) => {
|
|
4513
|
+
if (depth > 4) return;
|
|
4514
|
+
let entries = [];
|
|
4515
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
4516
|
+
for (const e of entries) {
|
|
4517
|
+
const p = join(dir, e.name);
|
|
4518
|
+
if (e.isDirectory()) { walk(p, depth + 1); continue; }
|
|
4519
|
+
if (!/\.lnk$/i.test(e.name)) continue;
|
|
4520
|
+
const name = e.name.replace(/\.lnk$/i, '');
|
|
4521
|
+
if (SKIP.test(name)) continue;
|
|
4522
|
+
const key = slugify(name, '');
|
|
4523
|
+
if (key && !apps[key]) apps[key] = p;
|
|
4524
|
+
}
|
|
4525
|
+
};
|
|
4526
|
+
for (const r of roots) if (r) walk(r);
|
|
4527
|
+
return apps;
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
/** Installed .app bundles, so macOS gets the same "everything on this machine" list. */
|
|
4531
|
+
function scanMacApplications() {
|
|
4532
|
+
const apps = {};
|
|
4533
|
+
for (const dir of ['/Applications', '/System/Applications', join(os.homedir(), 'Applications')]) {
|
|
4534
|
+
let entries = [];
|
|
4535
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
4536
|
+
for (const e of entries) {
|
|
4537
|
+
if (!e.name.endsWith('.app')) continue;
|
|
4538
|
+
const key = slugify(e.name.replace(/\.app$/, ''), '');
|
|
4539
|
+
if (key && !apps[key]) apps[key] = join(dir, e.name);
|
|
4540
|
+
}
|
|
4135
4541
|
}
|
|
4136
4542
|
return apps;
|
|
4137
4543
|
}
|
|
@@ -4988,9 +5394,19 @@ async function writeHostControlAccess(projectDir, cfg) {
|
|
|
4988
5394
|
'',
|
|
4989
5395
|
'- macOS: `open -a "<Tên app>"` — ví dụ `open -a "TeamViewer"`',
|
|
4990
5396
|
'- Linux: `xdg-open <app|url>` hoặc chạy binary trực tiếp',
|
|
4991
|
-
|
|
5397
|
+
...(process.platform === 'win32' ? [
|
|
5398
|
+
'- Windows: **đừng** gọi `Start-Process <TênApp>` — tên app KHÔNG phải lệnh trên PATH, bạn sẽ nhận',
|
|
5399
|
+
' `The system cannot find the file specified`. Hãy tra shortcut trong Start Menu rồi mở nó:',
|
|
5400
|
+
'',
|
|
5401
|
+
'```sh',
|
|
5402
|
+
'powershell -NoProfile -Command "$n=\'TeamViewer\'; $r=@(\"$env:ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\",\"$env:APPDATA\\Microsoft\\Windows\\Start Menu\\Programs\"); $l=Get-ChildItem $r -Recurse -Filter \"*$n*.lnk\" -EA SilentlyContinue | Select -First 1; if($l){ Start-Process $l.FullName } else { \"khong tim thay $n\" }"',
|
|
5403
|
+
'```',
|
|
5404
|
+
'',
|
|
5405
|
+
'Đổi `TeamViewer` thành tên app cần mở. Cách này đúng với MỌI app đã cài, kể cả app chủ mới cài hôm qua,',
|
|
5406
|
+
'vì Windows luôn đặt shortcut vào Start Menu và `.lnk` tự biết đường dẫn thật.',
|
|
5407
|
+
] : ['- Windows: `cmd /c start "" "<app>"`']),
|
|
4992
5408
|
'',
|
|
4993
|
-
apps.length ? `App
|
|
5409
|
+
apps.length ? `App có trên máy này: ${apps.map((a) => `\`${a}\``).join(', ')}.` : 'Chủ chưa liệt kê app — mở theo tên app có trên máy.',
|
|
4994
5410
|
...captureBlock,
|
|
4995
5411
|
...scriptBlock,
|
|
4996
5412
|
'',
|
|
@@ -5292,6 +5708,79 @@ async function waitForDockerDaemon(timeoutMs) {
|
|
|
5292
5708
|
}
|
|
5293
5709
|
}
|
|
5294
5710
|
|
|
5711
|
+
// OpenClaw's own engines range, copied verbatim from its package.json:
|
|
5712
|
+
// ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0"
|
|
5713
|
+
// The gaps matter — Node 23.x and 25.0–25.8 are NOT usable — so "install the latest Node" is the
|
|
5714
|
+
// wrong instinct here. When we have to install, we install 24 LTS, which sits inside a supported
|
|
5715
|
+
// window and is what every working host in the fleet already runs.
|
|
5716
|
+
const NODE_TARGET_MAJOR = 24;
|
|
5717
|
+
function nodeVersionSupported(raw) {
|
|
5718
|
+
const m = String(raw || '').match(/v?(\d+)\.(\d+)\.(\d+)/);
|
|
5719
|
+
if (!m) return false;
|
|
5720
|
+
const [maj, min, pat] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
5721
|
+
const atLeast = (a, b, c) => maj > a || (maj === a && (min > b || (min === b && pat >= c)));
|
|
5722
|
+
if (maj === 22) return atLeast(22, 22, 3);
|
|
5723
|
+
if (maj === 24) return atLeast(24, 15, 0);
|
|
5724
|
+
if (maj >= 25) return maj > 25 || atLeast(25, 9, 0);
|
|
5725
|
+
return false;
|
|
5726
|
+
}
|
|
5727
|
+
|
|
5728
|
+
// Native mode runs openclaw straight off the host, so the host's Node IS the runtime — unlike
|
|
5729
|
+
// docker mode, where the image carried its own. A customer machine that never needed Node before
|
|
5730
|
+
// (or carries one outside openclaw's supported range) must get a usable one before anything else,
|
|
5731
|
+
// otherwise the install dies deep inside `npm i -g openclaw` with an unreadable engine error.
|
|
5732
|
+
async function ensureNodeInstalled() {
|
|
5733
|
+
const current = await commandExists('node', ['--version']);
|
|
5734
|
+
if (current.ok && nodeVersionSupported(current.output)) {
|
|
5735
|
+
sendLog(`[node] Node ${current.output.trim()} hợp lệ.`);
|
|
5736
|
+
return;
|
|
5737
|
+
}
|
|
5738
|
+
const why = current.ok
|
|
5739
|
+
? `Node ${current.output.trim()} nằm ngoài dải OpenClaw hỗ trợ`
|
|
5740
|
+
: 'Máy chưa có Node';
|
|
5741
|
+
sendLog(`[node] ${why} — đang cài Node ${NODE_TARGET_MAJOR} LTS...`);
|
|
5742
|
+
|
|
5743
|
+
if (process.platform === 'linux') {
|
|
5744
|
+
const root = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
5745
|
+
const sudo = root ? '' : 'sudo ';
|
|
5746
|
+
// NodeSource covers deb/rpm and pins the major, so we land inside the supported window
|
|
5747
|
+
// instead of whatever "latest" happens to be that week.
|
|
5748
|
+
await run('sh', ['-c',
|
|
5749
|
+
`curl -fsSL https://deb.nodesource.com/setup_${NODE_TARGET_MAJOR}.x | ${sudo}bash - && ${sudo}apt-get install -y nodejs`,
|
|
5750
|
+
]).catch(async () => {
|
|
5751
|
+
sendLog('[node] apt không dùng được — thử nvm...');
|
|
5752
|
+
await run('sh', ['-c',
|
|
5753
|
+
`export NVM_DIR="$HOME/.nvm"; curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash && . "$NVM_DIR/nvm.sh" && nvm install ${NODE_TARGET_MAJOR} && nvm alias default ${NODE_TARGET_MAJOR}`,
|
|
5754
|
+
]);
|
|
5755
|
+
});
|
|
5756
|
+
} else if (process.platform === 'darwin') {
|
|
5757
|
+
const brew = await commandExists('brew', ['--version']);
|
|
5758
|
+
if (!brew.ok) throw httpError(400, 'macOS: cần Homebrew để tự cài Node. Cài tại https://brew.sh rồi thử lại.');
|
|
5759
|
+
await run('brew', ['install', `node@${NODE_TARGET_MAJOR}`]);
|
|
5760
|
+
await run('brew', ['link', '--overwrite', '--force', `node@${NODE_TARGET_MAJOR}`]).catch(() => {});
|
|
5761
|
+
} else if (process.platform === 'win32') {
|
|
5762
|
+
const winget = await commandExists('winget', ['--version']);
|
|
5763
|
+
const choco = await commandExists('choco', ['--version']);
|
|
5764
|
+
if (winget.ok) {
|
|
5765
|
+
await run('winget', ['install', '-e', '--id', `OpenJS.NodeJS.LTS`, '--accept-source-agreements', '--accept-package-agreements']);
|
|
5766
|
+
} else if (choco.ok) {
|
|
5767
|
+
await run('choco', ['install', 'nodejs-lts', '-y']);
|
|
5768
|
+
} else {
|
|
5769
|
+
throw httpError(400, `Windows: cần winget hoặc Chocolatey để tự cài Node (hoặc cài Node ${NODE_TARGET_MAJOR} LTS thủ công tại https://nodejs.org) rồi thử lại.`);
|
|
5770
|
+
}
|
|
5771
|
+
} else {
|
|
5772
|
+
throw httpError(400, `Hệ điều hành không tự cài Node được. Hãy cài Node ${NODE_TARGET_MAJOR} LTS thủ công rồi thử lại.`);
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
// A fresh install lands on a PATH this process never picked up, so re-check through a new shell
|
|
5776
|
+
// rather than trusting our own env.
|
|
5777
|
+
const after = await commandExists('node', ['--version']);
|
|
5778
|
+
if (!after.ok || !nodeVersionSupported(after.output)) {
|
|
5779
|
+
throw httpError(500, `Đã cài Node nhưng phiên bản hiện tại (${after.output || 'không đọc được'}) vẫn ngoài dải OpenClaw hỗ trợ. Mở lại cửa sổ dòng lệnh (hoặc đăng xuất/đăng nhập lại) để PATH cập nhật, rồi cài lại.`);
|
|
5780
|
+
}
|
|
5781
|
+
sendLog(`[node] Node ${after.output.trim()} đã sẵn sàng.`);
|
|
5782
|
+
}
|
|
5783
|
+
|
|
5295
5784
|
// Ensure Docker (engine + compose) is available before a docker-mode install, auto-installing the
|
|
5296
5785
|
// latest version appropriate for the host OS when it is missing:
|
|
5297
5786
|
// • Linux → Docker's official convenience script (get.docker.com; auto-detects the distro) + start daemon
|
|
@@ -5391,9 +5880,15 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
|
|
|
5391
5880
|
// Make sure Docker is present (auto-install on Linux/VPS) before doing any work — fail fast
|
|
5392
5881
|
// with a clear message rather than deep inside `docker compose up`. Native mode has no
|
|
5393
5882
|
// container, so it skips this entirely (that is much of the point of choosing it).
|
|
5394
|
-
|
|
5883
|
+
// Native has no image to carry a runtime, so the host's Node IS the runtime — get a supported
|
|
5884
|
+
// one on the machine before npm touches anything. Docker still needs the daemon instead.
|
|
5885
|
+
if (mode === 'native') await ensureNodeInstalled();
|
|
5886
|
+
else await ensureDockerInstalled(osChoice);
|
|
5395
5887
|
await writeCoreProject({ projectDir, osChoice, mode, gatewayPort, routerPort, userTimezone });
|
|
5396
5888
|
await run('npm', ['install', '-g', OPENCLAW_NPM_SPEC]);
|
|
5889
|
+
// The global runtime just changed — every version gate after this point (notably
|
|
5890
|
+
// prepareNativeStateHome) must read the NEW version, not the one this host booted with.
|
|
5891
|
+
invalidateHostOpenclawVersion();
|
|
5397
5892
|
await run('npm', ['install', '-g', NINE_ROUTER_NPM_SPEC]);
|
|
5398
5893
|
if (mode === 'docker') {
|
|
5399
5894
|
const dockerDir = join(projectDir, 'docker', 'openclaw');
|
|
@@ -5613,6 +6108,34 @@ async function discoverDockerBotProjectRoots() {
|
|
|
5613
6108
|
// dir symlinked to it — without this filter the account home shows up as a phantom "root"
|
|
5614
6109
|
// project tab after every refresh (measured on vps_c-thu). The native marker's `label` was
|
|
5615
6110
|
// written from the ORIGINAL project dir, so a mismatch identifies the phantom.
|
|
6111
|
+
/**
|
|
6112
|
+
* Two paths can be the same project: a native install links the account home and the project dir
|
|
6113
|
+
* together, so `~/.openclaw` and `<project>/.openclaw` resolve to one directory on disk. Scanning
|
|
6114
|
+
* finds both and the dashboard then lists the SAME bots twice under two project names (seen on
|
|
6115
|
+
* win_kha as "bot" and "VT 2025", 4 identical bots online in each — 10/09/2026).
|
|
6116
|
+
*
|
|
6117
|
+
* Dedupe on where `.openclaw` really lands, and keep the entry that IS the real directory: the
|
|
6118
|
+
* link side is an artefact of the install layout, not somewhere the customer put a project.
|
|
6119
|
+
* Works whichever way the link points, which matters because the direction differs between a
|
|
6120
|
+
* fresh native install and one migrated over from docker.
|
|
6121
|
+
*/
|
|
6122
|
+
function dedupeProjectsByRealState(dirs) {
|
|
6123
|
+
const byReal = new Map();
|
|
6124
|
+
for (const dir of dirs) {
|
|
6125
|
+
if (!dir) continue;
|
|
6126
|
+
const statePath = join(dir, '.openclaw');
|
|
6127
|
+
let real = statePath;
|
|
6128
|
+
let isLink = false;
|
|
6129
|
+
try { real = fs.realpathSync(statePath); } catch {}
|
|
6130
|
+
try { isLink = fs.lstatSync(statePath).isSymbolicLink(); } catch {}
|
|
6131
|
+
const key = resolve(real).toLowerCase();
|
|
6132
|
+
const prev = byReal.get(key);
|
|
6133
|
+
// First one wins, unless we already kept a link and now meet the real directory.
|
|
6134
|
+
if (!prev || (prev.isLink && !isLink)) byReal.set(key, { dir, isLink });
|
|
6135
|
+
}
|
|
6136
|
+
return [...byReal.values()].map((v) => v.dir);
|
|
6137
|
+
}
|
|
6138
|
+
|
|
5616
6139
|
function isPhantomStateDirProject(dir) {
|
|
5617
6140
|
try {
|
|
5618
6141
|
const meta = readNativeMeta(dir);
|
|
@@ -5644,7 +6167,7 @@ async function discoverNativeProjectRoots(rootProjectDir) {
|
|
|
5644
6167
|
if (rootProjectDir && existsSync(nativeMarkerPath(rootProjectDir)) && existsSync(join(rootProjectDir, '.openclaw', 'openclaw.json'))) {
|
|
5645
6168
|
roots.add(resolve(rootProjectDir));
|
|
5646
6169
|
}
|
|
5647
|
-
return [...roots].filter((dir) => !isPhantomStateDirProject(dir));
|
|
6170
|
+
return dedupeProjectsByRealState([...roots].filter((dir) => !isPhantomStateDirProject(dir)));
|
|
5648
6171
|
}
|
|
5649
6172
|
|
|
5650
6173
|
async function findLatestProject(rootProjectDir) {
|
|
@@ -5699,7 +6222,10 @@ async function findLatestProject(rootProjectDir) {
|
|
|
5699
6222
|
candidates.push({ dir: dr, mtimeMs: (st?.mtimeMs || 0) + 1e15 });
|
|
5700
6223
|
}
|
|
5701
6224
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
5702
|
-
|
|
6225
|
+
// Drop the linked twin before picking a winner, or the account-home view of the same project
|
|
6226
|
+
// can outrank the project itself purely on mtime.
|
|
6227
|
+
const keep = new Set(dedupeProjectsByRealState(candidates.map((c) => c.dir)));
|
|
6228
|
+
return candidates.find((c) => keep.has(c.dir))?.dir || candidates[0]?.dir || null;
|
|
5703
6229
|
}
|
|
5704
6230
|
|
|
5705
6231
|
async function ensureProjectsLoaded(rootProjectDir) {
|
|
@@ -5710,7 +6236,10 @@ async function ensureProjectsLoaded(rootProjectDir) {
|
|
|
5710
6236
|
try {
|
|
5711
6237
|
const saved = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
5712
6238
|
if (Array.isArray(saved?.projects)) {
|
|
5713
|
-
state
|
|
6239
|
+
// Filter on the way IN: a linked twin already written to the state file would otherwise
|
|
6240
|
+
// survive every later dedupe, because the dashboard reads state.projects directly.
|
|
6241
|
+
const keep = new Set(dedupeProjectsByRealState(saved.projects.map((p) => p && p.projectDir)));
|
|
6242
|
+
state.projects = saved.projects.filter((p) => p && keep.has(p.projectDir));
|
|
5714
6243
|
}
|
|
5715
6244
|
} catch {}
|
|
5716
6245
|
}
|
|
@@ -5755,6 +6284,11 @@ async function computeDiscoverProjects(rootProjectDir) {
|
|
|
5755
6284
|
// Drop phantom state-dir "projects" that slipped into the saved list (e.g. the account
|
|
5756
6285
|
// home after a 2026.8.x native state move) — and keep them out of the persisted state.
|
|
5757
6286
|
state.projects = state.projects.filter((p) => !isPhantomStateDirProject(p.projectDir));
|
|
6287
|
+
// The label check above only catches projects that carry a marker with a mismatched label.
|
|
6288
|
+
// A linked twin without one still slips through and the dashboard shows the same bots under
|
|
6289
|
+
// two project names, both "online" — collapse those onto the real directory.
|
|
6290
|
+
const keepDirs = new Set(dedupeProjectsByRealState(state.projects.map((p) => p.projectDir)));
|
|
6291
|
+
state.projects = state.projects.filter((p) => keepDirs.has(p.projectDir));
|
|
5758
6292
|
|
|
5759
6293
|
// In parallel: each buildProjectMeta runs runtime detection (docker calls, port probes), so a
|
|
5760
6294
|
// handful of projects turned into seconds of dashboard load when this was a sequential loop.
|
|
@@ -6916,6 +7450,18 @@ async function handler(req, res, rootProjectDir) {
|
|
|
6916
7450
|
// capture / recording and node-script permissions get granted (opt out with grants:false).
|
|
6917
7451
|
const granted = cfg.enabled && body.grants !== false ? grantHostCapabilities(cfg) : [];
|
|
6918
7452
|
if (granted.length) sendLog(`[host-control] Đã cấp thêm quyền chạy: ${granted.join(', ')}.`);
|
|
7453
|
+
// Pressing this button means "let the bot drive this machine", so it must deliver the real
|
|
7454
|
+
// thing — OpenClaw's own `computer` tool (screenshot → click → type → drag), not just the
|
|
7455
|
+
// ability to launch an app. That tool needs three separate pieces switched on together, and
|
|
7456
|
+
// any one missing leaves the bot insisting it has no permission:
|
|
7457
|
+
// • the tool allowed for agents, • the cua-computer plugin (mandatory on Windows),
|
|
7458
|
+
// • a running node host advertising computer.act + screen.snapshot.
|
|
7459
|
+
// Doing it here rather than in a side script matters: the node host must live in a real
|
|
7460
|
+
// interactive desktop session, and the operator pressing this button IS in one.
|
|
7461
|
+
const computerUse = await setComputerUse(projectDir, cfg.enabled).catch((e) => {
|
|
7462
|
+
sendLog(`[host-control] computer-use: ${e.message}`);
|
|
7463
|
+
return { ok: false, error: e.message };
|
|
7464
|
+
});
|
|
6919
7465
|
await fsp.writeFile(hostControlConfigPath(projectDir), JSON.stringify(cfg, null, 2), 'utf8');
|
|
6920
7466
|
let started = { ok: false, reason: 'disabled' };
|
|
6921
7467
|
if (cfg.enabled) started = await ensureHostControl(projectDir);
|
|
@@ -6923,6 +7469,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
6923
7469
|
// disabling strips it so a bot never keeps instructions for an endpoint now refusing.
|
|
6924
7470
|
await writeHostControlAccess(projectDir, cfg).catch(() => {});
|
|
6925
7471
|
sendLog(`[host-control] ${cfg.enabled ? 'Đã BẬT' : 'Đã TẮT'} quyền điều khiển máy cho bot.`);
|
|
7472
|
+
if (cfg.enabled && computerUse?.ok) sendLog('[host-control] Bot có thể chụp màn hình, click chuột, gõ phím trên máy này.');
|
|
6926
7473
|
// Make sure the Codex desktop app can actually do GUI work, so `codex exec` is enough for
|
|
6927
7474
|
// the bot: install computer-use into the app and repair its MCP registration. Nothing is
|
|
6928
7475
|
// installed into the OpenClaw project and the gateway never restarts.
|
|
@@ -7414,4 +7961,4 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
|
|
|
7414
7961
|
]).catch(() => {});
|
|
7415
7962
|
}
|
|
7416
7963
|
|
|
7417
|
-
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, detectOs, stripCliWarnings, migrationLeaseDeadline, ensureNativePlugins, findFreeHostPort, syncNativeServiceEnv, adoptStrayNativeHome, runNativeConfigMigrations, detectExistingSetupUi, hardenNativeServiceRestarts, clearNativeServiceFailure, describePortHolder, reportNativeGatewayBlockage };
|
|
7964
|
+
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, detectOs, stripCliWarnings, migrationLeaseDeadline, ensureNativePlugins, findFreeHostPort, syncNativeServiceEnv, adoptStrayNativeHome, runNativeConfigMigrations, detectExistingSetupUi, nodeVersionSupported, migrateDockerProjectToNative, dedupeProjectsByRealState, hardenNativeServiceRestarts, clearNativeServiceFailure, describePortHolder, reportNativeGatewayBlockage };
|
|
@@ -230,6 +230,23 @@
|
|
|
230
230
|
cfg.messages = { ackReaction: '/-heart', ackReactionScope: 'all' };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
// ── session scope ─────────────────────────────────────────────────────────
|
|
234
|
+
// OpenClaw's own default (`main`) funnels EVERY direct message from EVERY person into a
|
|
235
|
+
// single shared session. Its runtime calls that out as a security problem verbatim:
|
|
236
|
+
// "⚠️ SECURITY: Multiple users sharing DM session.
|
|
237
|
+
// Configure \"session.dmScope: per-channel-peer\" in OpenClaw config."
|
|
238
|
+
// Two things go wrong in practice, both measured on vps_thuy-le 07/09/2026: the Control
|
|
239
|
+
// UI shows no DM session at all (there is nothing per-peer to list, so only groups
|
|
240
|
+
// appear), and the one shared session grows without bound until the model answers with
|
|
241
|
+
// an empty turn — "incomplete turn … payloads=0 tools=0" surfacing to the user as
|
|
242
|
+
// "⚠️ Agent couldn't generate a response".
|
|
243
|
+
// per-channel-peer (not per-peer) is the vendor's own wording and is the right split for
|
|
244
|
+
// a bot wired to several channels: the same person on Zalo and on Telegram stays apart.
|
|
245
|
+
// The key exists since 2026.7, so this is safe on every runtime setup installs.
|
|
246
|
+
// Only NEW configs get it — existing customer bots keep their current scope until
|
|
247
|
+
// someone changes it deliberately, because splitting sessions drops the old context.
|
|
248
|
+
cfg.session = { ...(cfg.session || {}), dmScope: 'per-channel-peer' };
|
|
249
|
+
|
|
233
250
|
// ── commands ──────────────────────────────────────────────────────────────
|
|
234
251
|
// No `ownerDisplay`: openclaw 2026.8.x strict schema rejects it as an unrecognized key
|
|
235
252
|
// and refuses to start (measured on vps_c-thu, 02/09/2026).
|
|
@@ -207,7 +207,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
207
207
|
// Gio gate theo version openclaw THAT trong container: <2026.8 thi backfill nhu cu, ≥2026.8 thi
|
|
208
208
|
// GO key neu con (tu chua cac project da nhiem). Khong doc duoc version thi KHONG dong nao chay
|
|
209
209
|
// — tha thieu mot default con hon gieo key lam gateway tu choi boot.
|
|
210
|
-
const contextDefaultsScript = `const fs=require('fs'),path=require('path');const p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));let ch=false;c.skills=c.skills||{};c.skills.workshop=c.skills.workshop||{};if(!c.skills.workshop.approvalPolicy){c.skills.workshop.approvalPolicy='auto';ch=true;}if(c.browser&&c.browser.enabled!==false){c.tools=c.tools||{};const dn=Array.isArray(c.tools.deny)?c.tools.deny:[];if(!dn.includes('browser')){dn.push('browser');c.tools.deny=dn;ch=true;}}let ocMajorMinor=0;try{const v=String(require('child_process').execSync('openclaw --version',{stdio:['ignore','pipe','ignore']})).match(/(\\d{4})\\.(\\d+)/);if(v)ocMajorMinor=Number(v[1])*100+Number(v[2]);}catch(e){}const d=(c.agents&&c.agents.defaults)?c.agents.defaults:null;if(d){if(d.imageMaxDimensionPx===undefined){d.imageMaxDimensionPx=1024;ch=true;}if(d.imageQuality===undefined){d.imageQuality='efficient';ch=true;}if(ocMajorMinor&&ocMajorMinor<202608){d.contextLimits=d.contextLimits||{};if(d.contextLimits.toolResultMaxChars===undefined){d.contextLimits.toolResultMaxChars=12000;ch=true;}}else if(ocMajorMinor>=202608&&d.contextLimits&&d.contextLimits.toolResultMaxChars!==undefined){delete d.contextLimits.toolResultMaxChars;ch=true;}}if(ocMajorMinor>=202609){if(c.messages&&c.messages.removeAckAfterReply!==undefined){delete c.messages.removeAckAfterReply;ch=true;console.log('[migrate] go messages.removeAckAfterReply (2026.9 khong con nhan khoa nay)');}if(c.meta&&c.meta.lastTouchedAt!==undefined){delete c.meta.lastTouchedAt;ch=true;console.log('[migrate] go meta.lastTouchedAt (2026.9 khong con nhan khoa nay)');}}c.gateway=(c.gateway&&typeof c.gateway==='object')?c.gateway:{};if(c.gateway.mode===undefined){c.gateway.mode='local';ch=true;console.log('[migrate] gateway.mode=local (2026.9 tu choi chay neu thieu)');}c.tools=c.tools||{};c.tools.sessions=(c.tools.sessions&&typeof c.tools.sessions==='object')?c.tools.sessions:{};if(c.tools.sessions.visibility===undefined){c.tools.sessions.visibility='agent';ch=true;console.log('[migrate] tools.sessions.visibility=agent (khong cho bot doc phien cua bot khac)');}if(ocMajorMinor>=202608&&d&&d.modelSelectionScope===undefined){d.modelSelectionScope='session';ch=true;console.log('[migrate] agents.defaults.modelSelectionScope=session (khong ghi de model trong config)');}if(ocMajorMinor>=202608){const ag=c.agents||{};const nEntries=(Array.isArray(ag.list)?ag.list.length:0)+(ag.entries&&typeof ag.entries==='object'?Object.keys(ag.entries).length:0);if(nEntries>1&&ag.ownership!=='explicit'){ag.ownership='explicit';c.agents=ag;ch=true;}try{const agRoot=path.join(process.cwd(),'.openclaw','agents');for(const id of fs.readdirSync(agRoot)){const sf=path.join(agRoot,id,'sessions','sessions.json');if(fs.existsSync(sf)){fs.renameSync(sf,sf+'.bak-legacy-'+Date.now());console.log('[migrate] parked legacy session store '+sf);}}}catch(e){}if(c.commands&&c.commands.ownerDisplay!==undefined){delete c.commands.ownerDisplay;ch=true;}if(c.plugins&&c.plugins.bundledDiscovery!==undefined){delete c.plugins.bundledDiscovery;ch=true;}if(Array.isArray(ag.list)){ag.entries=(ag.entries&&typeof ag.entries==='object'&&!Array.isArray(ag.entries))?ag.entries:{};for(const a of ag.list){if(a&&a.id&&!ag.entries[a.id]){const{id:_aid,...rest}=a;ag.entries[_aid]=rest;}}delete ag.list;c.agents=ag;ch=true;console.log('[migrate] moved agents.list into agents.entries');}try{const ea=path.join(process.cwd(),'.openclaw','exec-approvals.json');if(fs.existsSync(ea)){fs.renameSync(ea,ea+'.bak-legacy-'+Date.now());console.log('[migrate] parked legacy exec approvals '+ea);}}catch(e){}try{const ag2=c.agents||{};const ids=(ag2.entries&&typeof ag2.entries==='object')?Object.keys(ag2.entries):(Array.isArray(ag2.list)?ag2.list.map(function(a){return a&&a.id;}).filter(Boolean):[]);if(ids.length>1&&d){if(!(d.heartbeat&&d.heartbeat.agentId)){d.heartbeat=(d.heartbeat&&typeof d.heartbeat==='object')?d.heartbeat:{};d.heartbeat.agentId=ids[0];ch=true;console.log('[migrate] agents.defaults.heartbeat.agentId='+ids[0]+' (khong thi heartbeat tat cam)');}if(!(d.systemAgent&&d.systemAgent.agentId)){d.systemAgent=(d.systemAgent&&typeof d.systemAgent==='object')?d.systemAgent:{};d.systemAgent.agentId=ids[0];ch=true;console.log('[migrate] agents.defaults.systemAgent.agentId='+ids[0]+' (khong thi cron.list vo ca trang Automations)');}const tk=(c.talk&&typeof c.talk==='object')?c.talk:{};if(!tk.agentId){tk.agentId=ids[0];c.talk=tk;ch=true;console.log('[migrate] talk.agentId='+ids[0]+' (khong thi talk.catalog loi khi mo Control UI)');}}}catch(e){}}const pr=c.models&&c.models.providers&&c.models.providers['9router'];if(pr&&Array.isArray(pr.models)){for(const m of pr.models){if(m&&m.id==='smart-route'&&(m.contextWindow===200000||m.contextWindow===131072)){m.contextWindow=1048576;ch=true;}}}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
|
|
210
|
+
const contextDefaultsScript = `const fs=require('fs'),path=require('path');const p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));let ch=false;c.skills=c.skills||{};c.skills.workshop=c.skills.workshop||{};if(!c.skills.workshop.approvalPolicy){c.skills.workshop.approvalPolicy='auto';ch=true;}if(c.browser&&c.browser.enabled!==false){c.tools=c.tools||{};const dn=Array.isArray(c.tools.deny)?c.tools.deny:[];if(!dn.includes('browser')){dn.push('browser');c.tools.deny=dn;ch=true;}}let ocMajorMinor=0;try{const v=String(require('child_process').execSync('openclaw --version',{stdio:['ignore','pipe','ignore']})).match(/(\\d{4})\\.(\\d+)/);if(v)ocMajorMinor=Number(v[1])*100+Number(v[2]);}catch(e){}const d=(c.agents&&c.agents.defaults)?c.agents.defaults:null;if(d){if(d.imageMaxDimensionPx===undefined){d.imageMaxDimensionPx=1024;ch=true;}if(d.imageQuality===undefined){d.imageQuality='efficient';ch=true;}if(ocMajorMinor&&ocMajorMinor<202608){d.contextLimits=d.contextLimits||{};if(d.contextLimits.toolResultMaxChars===undefined){d.contextLimits.toolResultMaxChars=12000;ch=true;}}else if(ocMajorMinor>=202608&&d.contextLimits&&d.contextLimits.toolResultMaxChars!==undefined){delete d.contextLimits.toolResultMaxChars;ch=true;}}if(ocMajorMinor>=202609){if(c.messages&&c.messages.removeAckAfterReply!==undefined){delete c.messages.removeAckAfterReply;ch=true;console.log('[migrate] go messages.removeAckAfterReply (2026.9 khong con nhan khoa nay)');}if(c.meta&&c.meta.lastTouchedAt!==undefined){delete c.meta.lastTouchedAt;ch=true;console.log('[migrate] go meta.lastTouchedAt (2026.9 khong con nhan khoa nay)');}if(c.browser&&c.browser.profiles&&typeof c.browser.profiles==='object'){for(const pn of Object.keys(c.browser.profiles)){const bp=c.browser.profiles[pn];if(bp&&typeof bp==='object'&&bp.color!==undefined){delete bp.color;ch=true;console.log('[migrate] go browser.profiles.'+pn+'.color (2026.9 khong con nhan khoa nay)');}}}}c.gateway=(c.gateway&&typeof c.gateway==='object')?c.gateway:{};if(c.gateway.mode===undefined){c.gateway.mode='local';ch=true;console.log('[migrate] gateway.mode=local (2026.9 tu choi chay neu thieu)');}c.tools=c.tools||{};c.tools.sessions=(c.tools.sessions&&typeof c.tools.sessions==='object')?c.tools.sessions:{};if(c.tools.sessions.visibility===undefined){c.tools.sessions.visibility='agent';ch=true;console.log('[migrate] tools.sessions.visibility=agent (khong cho bot doc phien cua bot khac)');}if(ocMajorMinor>=202608&&d&&d.modelSelectionScope===undefined){d.modelSelectionScope='session';ch=true;console.log('[migrate] agents.defaults.modelSelectionScope=session (khong ghi de model trong config)');}if(ocMajorMinor>=202608){const ag=c.agents||{};const nEntries=(Array.isArray(ag.list)?ag.list.length:0)+(ag.entries&&typeof ag.entries==='object'?Object.keys(ag.entries).length:0);if(nEntries>1&&ag.ownership!=='explicit'){ag.ownership='explicit';c.agents=ag;ch=true;}try{const agRoot=path.join(process.cwd(),'.openclaw','agents');for(const id of fs.readdirSync(agRoot)){const sf=path.join(agRoot,id,'sessions','sessions.json');if(fs.existsSync(sf)){fs.renameSync(sf,sf+'.bak-legacy-'+Date.now());console.log('[migrate] parked legacy session store '+sf);}}}catch(e){}if(c.commands&&c.commands.ownerDisplay!==undefined){delete c.commands.ownerDisplay;ch=true;}if(c.plugins&&c.plugins.bundledDiscovery!==undefined){delete c.plugins.bundledDiscovery;ch=true;}if(Array.isArray(ag.list)){ag.entries=(ag.entries&&typeof ag.entries==='object'&&!Array.isArray(ag.entries))?ag.entries:{};for(const a of ag.list){if(a&&a.id&&!ag.entries[a.id]){const{id:_aid,...rest}=a;ag.entries[_aid]=rest;}}delete ag.list;c.agents=ag;ch=true;console.log('[migrate] moved agents.list into agents.entries');}try{const ea=path.join(process.cwd(),'.openclaw','exec-approvals.json');if(fs.existsSync(ea)){fs.renameSync(ea,ea+'.bak-legacy-'+Date.now());console.log('[migrate] parked legacy exec approvals '+ea);}}catch(e){}try{const ag2=c.agents||{};const ids=(ag2.entries&&typeof ag2.entries==='object')?Object.keys(ag2.entries):(Array.isArray(ag2.list)?ag2.list.map(function(a){return a&&a.id;}).filter(Boolean):[]);if(ids.length>1&&d){if(!(d.heartbeat&&d.heartbeat.agentId)){d.heartbeat=(d.heartbeat&&typeof d.heartbeat==='object')?d.heartbeat:{};d.heartbeat.agentId=ids[0];ch=true;console.log('[migrate] agents.defaults.heartbeat.agentId='+ids[0]+' (khong thi heartbeat tat cam)');}if(!(d.systemAgent&&d.systemAgent.agentId)){d.systemAgent=(d.systemAgent&&typeof d.systemAgent==='object')?d.systemAgent:{};d.systemAgent.agentId=ids[0];ch=true;console.log('[migrate] agents.defaults.systemAgent.agentId='+ids[0]+' (khong thi cron.list vo ca trang Automations)');}const tk=(c.talk&&typeof c.talk==='object')?c.talk:{};if(!tk.agentId){tk.agentId=ids[0];c.talk=tk;ch=true;console.log('[migrate] talk.agentId='+ids[0]+' (khong thi talk.catalog loi khi mo Control UI)');}}}catch(e){}}const pr=c.models&&c.models.providers&&c.models.providers['9router'];if(pr&&Array.isArray(pr.models)){for(const m of pr.models){if(m&&m.id==='smart-route'&&(m.contextWindow===200000||m.contextWindow===131072)){m.contextWindow=1048576;ch=true;}}}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
|
|
211
211
|
|
|
212
212
|
function buildDockerArtifacts(options) {
|
|
213
213
|
const {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Double-click launchers for a NATIVE bot on Windows.
|
|
5
|
+
*
|
|
6
|
+
* Why these exist instead of a Scheduled Task: `openclaw daemon install` creates a task with
|
|
7
|
+
* `LogonType: Interactive`, which means the gateway is tied to whoever's login session started
|
|
8
|
+
* it. Two consequences measured on win_kha (10/09/2026):
|
|
9
|
+
* • anything started over SSH dies the moment the session closes (LastTaskResult 0xC000013A);
|
|
10
|
+
* • a task set to run at boot is refused outright, because "at startup" and "only while
|
|
11
|
+
* someone is logged in" contradict each other (0x800710E0).
|
|
12
|
+
* Making it run without a login needs the account password stored in the task — Task Scheduler
|
|
13
|
+
* rejected that too ("The user account is unknown, the password is incorrect"). So the owner
|
|
14
|
+
* drives it by hand, exactly like the .command files on macOS.
|
|
15
|
+
*
|
|
16
|
+
* Every trap below cost a round trip with a customer waiting, so they are encoded here:
|
|
17
|
+
*
|
|
18
|
+
* 1. `.cmd` MUST be CRLF. LF-only files break on Windows.
|
|
19
|
+
* 2. Never name a launcher after the command it calls. `9router.cmd` in the project dir plus
|
|
20
|
+
* `cd /d <project>` makes `call 9router` re-enter the same file — BATCH RECURSION, and the
|
|
21
|
+
* process dies before doing anything.
|
|
22
|
+
* 3. Never append to a fixed log file. 9router holds its log open for its whole lifetime, so
|
|
23
|
+
* the SECOND run cannot open it and exits with "The process cannot access the file because
|
|
24
|
+
* it is being used by another process" — invisible on the first run, which is why it is easy
|
|
25
|
+
* to ship broken.
|
|
26
|
+
* 4. 9router shows a `Choose Interface` menu when its stdout looks like a real console. Started
|
|
27
|
+
* hidden, nobody can answer it, so it hangs and never opens its port. `> NUL` is what tells
|
|
28
|
+
* it to run unattended. Symptom without it: works when run by hand, silently dead in the
|
|
29
|
+
* background — same command.
|
|
30
|
+
* 5. `Start-Process -WindowStyle Hidden` does NOT hide a console a .cmd opens for itself. Only
|
|
31
|
+
* WScript.Shell.Run(..., 0, False) truly hides it. Leaving a visible window is not cosmetic:
|
|
32
|
+
* it IS the bot, and closing it kills the bot.
|
|
33
|
+
* 6. Anchor the working directory. Agents may carry relative workspace paths, which resolve
|
|
34
|
+
* against the launcher's cwd — move the files into a folder and those bots die with
|
|
35
|
+
* WORKSPACE_VANISHED while the others keep working.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const CRLF = (s) => s.replace(/\r?\n/g, '\r\n');
|
|
39
|
+
|
|
40
|
+
/** Hidden-launch helper. WScript is the only reliable way to start a .cmd with no window. */
|
|
41
|
+
function buildRunHiddenVbs() {
|
|
42
|
+
return CRLF(
|
|
43
|
+
'Set sh = CreateObject("WScript.Shell")\n' +
|
|
44
|
+
'sh.Run "cmd /c """ & WScript.Arguments(0) & """", 0, False\n',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Starts 9router. Named start-9router.cmd on purpose — see trap 2. */
|
|
49
|
+
function build9RouterCmd({ projectDir, routerPort }) {
|
|
50
|
+
return CRLF(
|
|
51
|
+
'@echo off\n' +
|
|
52
|
+
'rem Ten tep KHONG duoc la "9router.cmd": trung ten thi lenh ben duoi goi lai chinh no.\n' +
|
|
53
|
+
'rem "> NUL" la bat buoc: neu dau ra la man hinh that, 9router bay menu "Choose Interface"\n' +
|
|
54
|
+
'rem roi cho nguoi chon va khong bao gio mo cong.\n' +
|
|
55
|
+
`cd /d ${projectDir}\n` +
|
|
56
|
+
`set "DATA_DIR=${projectDir}\\.9router"\n` +
|
|
57
|
+
`call 9router -n -l -H 127.0.0.1 -p ${routerPort} --skip-update > NUL 2>&1\n`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Starts the gateway. Calls node directly — see the --task-supervisor note below. */
|
|
62
|
+
function buildGatewayCmd({ projectDir, gatewayPort }) {
|
|
63
|
+
return CRLF(
|
|
64
|
+
'@echo off\n' +
|
|
65
|
+
'rem Goi thang node, KHONG qua gateway.cmd cua openclaw: tep do chay kem --task-supervisor,\n' +
|
|
66
|
+
'rem no di tim Scheduled Task de ban giao; khong co task thi no de them tien trinh + cua so.\n' +
|
|
67
|
+
'rem Khong redirect ra tep co dinh (xem trap 3) — openclaw tu ghi nhat ky trong %TEMP%\\openclaw.\n' +
|
|
68
|
+
`cd /d ${projectDir}\n` +
|
|
69
|
+
'set "HOME=%USERPROFILE%"\n' +
|
|
70
|
+
`set "OPENCLAW_GATEWAY_PORT=${gatewayPort}"\n` +
|
|
71
|
+
`set "OPENCLAW_PORT=${gatewayPort}"\n` +
|
|
72
|
+
'"%ProgramFiles%\\nodejs\\node.exe" --max-old-space-size=8192 ' +
|
|
73
|
+
`"%APPDATA%\\npm\\node_modules\\openclaw\\dist\\index.js" gateway --port ${gatewayPort}\n`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildSetupUiCmd({ projectDir, setupPort }) {
|
|
78
|
+
return CRLF(
|
|
79
|
+
'@echo off\n' +
|
|
80
|
+
'rem Ten tep nhat ky kem gio, de lan chay sau khong dung vao tep dang bi giu (trap 3).\n' +
|
|
81
|
+
`cd /d ${projectDir}\n` +
|
|
82
|
+
'for /f "tokens=1-4 delims=/: " %%a in ("%TIME%") do set "T=%%a%%b%%c"\n' +
|
|
83
|
+
`call npx create-openclaw-bot --host=127.0.0.1 --port=${setupPort} --no-open --project-dir=${projectDir}` +
|
|
84
|
+
` > "${projectDir}\\setup-ui-%T%.log" 2>&1\n`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildStartBotCmd({ projectDir, gatewayPort, routerPort }) {
|
|
89
|
+
const dashPort = gatewayPort + 1;
|
|
90
|
+
return CRLF(
|
|
91
|
+
'@echo off\n' +
|
|
92
|
+
'title Khoi dong bot OpenClaw\n' +
|
|
93
|
+
'color 0A\n' +
|
|
94
|
+
`cd /d ${projectDir}\n` +
|
|
95
|
+
'echo.\n' +
|
|
96
|
+
'echo ============================================\n' +
|
|
97
|
+
`echo KHOI DONG BOT -- ${projectDir}\n` +
|
|
98
|
+
'echo ============================================\n' +
|
|
99
|
+
'echo.\n' +
|
|
100
|
+
'echo Dang bat 9Router...\n' +
|
|
101
|
+
`powershell -NoProfile -Command "if (-not (Get-NetTCPConnection -LocalPort ${routerPort} -State Listen -EA SilentlyContinue)) { Start-Process wscript -ArgumentList '${projectDir}\\run-hidden.vbs','${projectDir}\\start-9router.cmd' -WindowStyle Hidden }" >nul 2>&1\n` +
|
|
102
|
+
'echo Cho 9Router san sang...\n' +
|
|
103
|
+
`powershell -NoProfile -Command "for($i=0;$i -lt 20;$i++){ try{ Invoke-WebRequest 'http://127.0.0.1:${routerPort}/' -UseBasicParsing -TimeoutSec 3 | Out-Null; break } catch { Start-Sleep -Seconds 2 } }"\n` +
|
|
104
|
+
'echo Dang bat bot (gateway)...\n' +
|
|
105
|
+
`powershell -NoProfile -Command "if (-not (Get-NetTCPConnection -LocalPort ${gatewayPort} -State Listen -EA SilentlyContinue)) { Start-Process wscript -ArgumentList '${projectDir}\\run-hidden.vbs','${projectDir}\\gateway-start.cmd' -WindowStyle Hidden }" >nul 2>&1\n` +
|
|
106
|
+
'echo Cho bot san sang (co the mat 30-60 giay)...\n' +
|
|
107
|
+
'echo.\n' +
|
|
108
|
+
`powershell -NoProfile -Command "$ok=$false; for($i=0;$i -lt 40;$i++){ try{ Invoke-WebRequest 'http://127.0.0.1:${gatewayPort}/health' -UseBasicParsing -TimeoutSec 3 | Out-Null; $ok=$true; break } catch { Start-Sleep -Seconds 3 } }; if($ok){ Write-Host ' [OK] Bot da chay.' -ForegroundColor Green } else { Write-Host ' [LOI] Bot chua len.' -ForegroundColor Red }"\n` +
|
|
109
|
+
'echo.\n' +
|
|
110
|
+
'echo --- Trang thai ---\n' +
|
|
111
|
+
`powershell -NoProfile -Command "foreach($x in @(@('Bot (gateway)','http://127.0.0.1:${gatewayPort}/health'),@('9Router','http://127.0.0.1:${routerPort}/'),@('Bang dieu khien Zalo','http://127.0.0.1:${dashPort}/dashboard'))){ try{ (Invoke-WebRequest $x[1] -UseBasicParsing -TimeoutSec 6) | Out-Null; Write-Host (' {0,-22} DANG CHAY' -f $x[0]) -ForegroundColor Green } catch { Write-Host (' {0,-22} TAT' -f $x[0]) -ForegroundColor Red } }"\n` +
|
|
112
|
+
'echo.\n' +
|
|
113
|
+
'echo Bot chay AN, khong co cua so nao de lo tay dong.\n' +
|
|
114
|
+
'echo Bot chi tat khi ban DANG XUAT / TAT may, hoac bam "3 - DUNG BOT".\n' +
|
|
115
|
+
'echo.\n' +
|
|
116
|
+
'timeout /t 12 >nul\n',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildOpenUiCmd({ projectDir, setupPort }) {
|
|
121
|
+
return CRLF(
|
|
122
|
+
'@echo off\n' +
|
|
123
|
+
'title Mo giao dien bot\n' +
|
|
124
|
+
'color 0B\n' +
|
|
125
|
+
`cd /d ${projectDir}\n` +
|
|
126
|
+
'echo.\n' +
|
|
127
|
+
'echo ============================================\n' +
|
|
128
|
+
'echo MO GIAO DIEN QUAN TRI BOT\n' +
|
|
129
|
+
'echo ============================================\n' +
|
|
130
|
+
'echo.\n' +
|
|
131
|
+
`powershell -NoProfile -Command "if (-not (Get-NetTCPConnection -LocalPort ${setupPort} -State Listen -EA SilentlyContinue)) { Write-Host ' Dang bat giao dien...'; Start-Process wscript -ArgumentList '${projectDir}\\run-hidden.vbs','${projectDir}\\setup-ui.cmd' -WindowStyle Hidden } else { Write-Host ' Giao dien dang chay san.' }"\n` +
|
|
132
|
+
'echo Cho giao dien san sang...\n' +
|
|
133
|
+
`powershell -NoProfile -Command "for($i=0;$i -lt 30;$i++){ try{ Invoke-WebRequest 'http://127.0.0.1:${setupPort}/' -UseBasicParsing -TimeoutSec 3 | Out-Null; break } catch { Start-Sleep -Seconds 2 } }"\n` +
|
|
134
|
+
`start "" http://127.0.0.1:${setupPort}\n` +
|
|
135
|
+
'echo.\n' +
|
|
136
|
+
'echo Da mo trinh duyet. Dong cua so nay duoc roi.\n' +
|
|
137
|
+
'timeout /t 8 >nul\n',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildStopBotCmd({ gatewayPort, routerPort }) {
|
|
142
|
+
return CRLF(
|
|
143
|
+
'@echo off\n' +
|
|
144
|
+
'title Dung bot OpenClaw\n' +
|
|
145
|
+
'color 0C\n' +
|
|
146
|
+
'echo.\n' +
|
|
147
|
+
'echo Dang dung bot va 9Router...\n' +
|
|
148
|
+
'powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name=\'node.exe\'\\" | Where-Object { $_.CommandLine -like \'*openclaw*gateway*\' -or $_.CommandLine -like \'*9router*\' } | ForEach-Object { Write-Host (\' dung PID \' + $_.ProcessId); Stop-Process -Id $_.ProcessId -Force -EA SilentlyContinue }"\n' +
|
|
149
|
+
'echo.\n' +
|
|
150
|
+
`powershell -NoProfile -Command "Start-Sleep -Seconds 3; foreach($x in @(@('Bot',${gatewayPort}),@('9Router',${routerPort}))){ $c=Get-NetTCPConnection -LocalPort $x[1] -State Listen -EA SilentlyContinue; if($c){ Write-Host (' {0,-10} VAN CHAY' -f $x[0]) -ForegroundColor Yellow } else { Write-Host (' {0,-10} da tat' -f $x[0]) -ForegroundColor Green } }"\n` +
|
|
151
|
+
'echo.\n' +
|
|
152
|
+
'pause\n',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildReadme({ projectDir, gatewayPort, routerPort, setupPort }) {
|
|
157
|
+
return CRLF(
|
|
158
|
+
'BOT OPENCLAW TREN MAY NAY - HUONG DAN NHANH\n' +
|
|
159
|
+
'===========================================\n\n' +
|
|
160
|
+
'Ba tep .cmd, bam dup la chay. Khong can go lenh.\n\n' +
|
|
161
|
+
' 1 - KHOI DONG BOT.cmd Bat bot len. Bam sau moi lan khoi dong may.\n' +
|
|
162
|
+
' 2 - MO GIAO DIEN.cmd Mo trang quan tri bot trong trinh duyet.\n' +
|
|
163
|
+
' 3 - DUNG BOT.cmd Tat bot (chi dung khi can).\n\n' +
|
|
164
|
+
'QUAN TRONG\n' +
|
|
165
|
+
'----------\n' +
|
|
166
|
+
'- Bot chay AN hoan toan: khong co cua so den nao nam tren man hinh.\n' +
|
|
167
|
+
'- Bot KHONG tu chay khi bat may. Sau moi lan khoi dong lai may,\n' +
|
|
168
|
+
' bam "1 - KHOI DONG BOT" mot lan la xong.\n' +
|
|
169
|
+
'- Bot se tat khi ban DANG XUAT hoac TAT may.\n' +
|
|
170
|
+
'- Dat cac tep nay o dau cung duoc, chung deu neo ve ' + projectDir + '.\n\n' +
|
|
171
|
+
'Cong dang dung:\n' +
|
|
172
|
+
` ${gatewayPort} bot (gateway)\n` +
|
|
173
|
+
` ${gatewayPort + 1} bang dieu khien Zalo\n` +
|
|
174
|
+
` ${routerPort} 9Router\n` +
|
|
175
|
+
` ${setupPort} giao dien quan tri\n`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** All launcher files for a native Windows project, as { relativeName: content }. */
|
|
180
|
+
function buildWindowsLaunchers({ projectDir, gatewayPort = 18789, routerPort = 20128, setupPort = 51789 }) {
|
|
181
|
+
const opts = { projectDir, gatewayPort, routerPort, setupPort };
|
|
182
|
+
return {
|
|
183
|
+
'run-hidden.vbs': buildRunHiddenVbs(),
|
|
184
|
+
'start-9router.cmd': build9RouterCmd(opts),
|
|
185
|
+
'gateway-start.cmd': buildGatewayCmd(opts),
|
|
186
|
+
'setup-ui.cmd': buildSetupUiCmd(opts),
|
|
187
|
+
'0 - DOC TRUOC.txt': buildReadme(opts),
|
|
188
|
+
'1 - KHOI DONG BOT.cmd': buildStartBotCmd(opts),
|
|
189
|
+
'2 - MO GIAO DIEN.cmd': buildOpenUiCmd(opts),
|
|
190
|
+
'3 - DUNG BOT.cmd': buildStopBotCmd(opts),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Which of the generated files belong on the Desktop rather than in the project folder. */
|
|
195
|
+
const WINDOWS_DESKTOP_LAUNCHERS = ['0 - DOC TRUOC.txt', '1 - KHOI DONG BOT.cmd', '2 - MO GIAO DIEN.cmd', '3 - DUNG BOT.cmd'];
|
|
196
|
+
|
|
197
|
+
const api = { buildWindowsLaunchers, WINDOWS_DESKTOP_LAUNCHERS };
|
|
198
|
+
if (typeof globalThis !== 'undefined') globalThis.__openclawWindowsLaunchers = api;
|
|
199
|
+
if (typeof exports !== 'undefined') Object.assign(exports, api);
|
package/dist/web/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const $ = (sel) => document.querySelector(sel);
|
|
2
|
-
const state = { tab: 'dashboard', system: null, install: null, files: [], catalog: { skills: [], plugins: [] }, logs: [], zaloLoginOpen: false, zaloLoginLines: [], zaloQrDataUrl: '', lang: localStorage.getItem('openclaw-lang') || 'vi', theme: localStorage.getItem('openclaw-theme') || 'dark', tz: localStorage.getItem('openclaw-tz') || 'Asia/Ho_Chi_Minh', navCollapsed: localStorage.getItem('openclaw-nav')==='1', os: null, mode: null, donateOpen: false, botModalOpen: false, botEditId: '', installModalOpen: false, fbPluginModalOpen: false, installTab: '
|
|
2
|
+
const state = { tab: 'dashboard', system: null, install: null, files: [], catalog: { skills: [], plugins: [] }, logs: [], zaloLoginOpen: false, zaloLoginLines: [], zaloQrDataUrl: '', lang: localStorage.getItem('openclaw-lang') || 'vi', theme: localStorage.getItem('openclaw-theme') || 'dark', tz: localStorage.getItem('openclaw-tz') || 'Asia/Ho_Chi_Minh', navCollapsed: localStorage.getItem('openclaw-nav')==='1', os: null, mode: null, donateOpen: false, botModalOpen: false, botEditId: '', installModalOpen: false, fbPluginModalOpen: false, installTab: 'native', installDraft: null, pathModal: null, confirmModal: null, botChannel: 'telegram', botPane: 'list', activeBotId: '', selectedFile: '', botMessage: '', projectConnectMessage: '', pendingProjectDir: '', selectedProjectDir: '', featureFlags: {}, featureInstalled: {}, featureLoading: {}, featureLocked: {}, zaloBackend: '', zaloHealth: null, openDirs: {} };
|
|
3
3
|
const SVG_CDN = 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons';
|
|
4
4
|
const OS_OPTIONS = [
|
|
5
5
|
{ id: 'win', title: 'Windows', subtitle: 'Auto-detected desktop', icon: `${SVG_CDN}/windows/default.svg`, badge: 'Desktop' },
|
|
@@ -212,19 +212,24 @@ function installModal() {
|
|
|
212
212
|
const sys = state.system || {};
|
|
213
213
|
const draft = refreshInstallDraft();
|
|
214
214
|
const os = draft.os || state.os || sys?.os || 'win';
|
|
215
|
-
const mode = draft.mode || state.installTab ||
|
|
215
|
+
const mode = draft.mode || state.installTab || 'native';
|
|
216
216
|
const pathExample = os === 'win' ? 'C:\\openclaw-setup' : os === 'macos' ? '/Users/you/openclaw-setup' : '/home/you/openclaw-setup';
|
|
217
217
|
const osChoices = OS_OPTIONS.map(o => [o.id, t(o.title, o.title), trChoice(o).subtitle]);
|
|
218
|
+
// Docker is closed for NEW projects: openclaw >=2026.9 writes config through fs-safe, which
|
|
219
|
+
// fstat()s the file after an atomic rename \u2014 a check that cannot pass through a Docker Desktop
|
|
220
|
+
// bind mount on Windows, so the first image rebuild after that takes the gateway down for good
|
|
221
|
+
// (measured on win_kha, 09/09/2026). Existing docker projects keep running and move to native on
|
|
222
|
+
// their next update; the tile stays visible but disabled so the reason is on screen.
|
|
218
223
|
const modeChoices = [
|
|
219
|
-
['
|
|
220
|
-
['
|
|
224
|
+
['native', 'Native', t('Ch\u1ea1y th\u1eb3ng tr\u00ean m\u00e1y n\u00e0y \u2014 kh\u00f4ng c\u1ea7n Docker, \u0111i\u1ec1u khi\u1ec3n \u0111\u01b0\u1ee3c app', 'Runs on this machine \u2014 no Docker, can drive desktop apps'), false],
|
|
225
|
+
['docker', 'Docker', t('\u0110\u00e3 ng\u1eebng \u2014 OpenClaw 2026.9 kh\u00f4ng ghi \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh qua Docker tr\u00ean Windows', 'Retired \u2014 OpenClaw 2026.9 cannot write config through Docker on Windows'), true],
|
|
221
226
|
];
|
|
222
227
|
return `<div class="modal-backdrop install-backdrop" data-install-modal="close">
|
|
223
228
|
<section class="donate-modal install-modal" role="dialog" aria-modal="true" aria-label="${t('T\u1ea1o Project','Create Project')}" onclick="event.stopPropagation()">
|
|
224
229
|
<button class="modal-x" data-install-modal="close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
225
230
|
<div class="donate-head"><span aria-hidden="true">+</span><div><p>${t('T\u1ea1o Project','Create Project')}</p><h2>${t('T\u1ea1o Project','Create Project')}</h2><small>${t('Ch\u1ecdn s\u1eb5n ch\u1ebf \u0111\u1ed9 \u1edf tab tr\u00ean. B\u00ean d\u01b0\u1edbi ch\u1ec9 c\u1ea7n ch\u1ecdn OS v\u00e0 nh\u1eadp ho\u1eb7c ch\u1ecdn \u0111\u01b0\u1eddng d\u1eabn project.','Mode stays in the tabs above. Below, choose OS and enter or pick the project path.')}</small></div></div>
|
|
226
231
|
<form id="install-form" class="install-form">
|
|
227
|
-
<div class="install-tabs">${modeChoices.map(([id,label,desc]) => `<button type="button" class="install-tab ${mode===id?'is-active':''}" data-install-set="mode" data-value="${id}"><strong>${escapeHtml(label)}</strong><small>${escapeHtml(desc)}</small></button>`).join('')}</div>
|
|
232
|
+
<div class="install-tabs">${modeChoices.map(([id,label,desc,locked]) => `<button type="button" class="install-tab ${mode===id?'is-active':''}${locked?' is-locked':''}" data-install-set="mode" data-value="${id}"${locked?' disabled aria-disabled="true"':''}><strong>${escapeHtml(label)}${locked?' 🔒':''}</strong><small>${escapeHtml(desc)}</small></button>`).join('')}</div>
|
|
228
233
|
<div class="install-grid install-grid--compact">
|
|
229
234
|
<div class="field wide"><span>${t('H\u1ec7 \u0111i\u1ec1u h\u00e0nh','Operating system')}</span>${pillGroup('os', os, osChoices)}<small>${t('\u0110\u00e3 ch\u1ecdn s\u1eb5n theo m\u00e1y \u0111ang ch\u1ea1y','Preselected from the current machine')}</small></div>
|
|
230
235
|
<label class="field wide"><span>${t('Đường dẫn project','Project path')}</span><input name="projectDir" placeholder="${escapeHtml(pathExample)}" value="${escapeHtml(draft.projectDir || pathExample)}" /><small>${t('Ví dụ: C:\\openclaw-setup hoặc /home/you/openclaw-setup. Bạn có thể tự sửa tên folder bot thành tên bất kỳ.','Example: C:\\openclaw-setup or /home/you/openclaw-setup. You can rename folder bot to any name.')}</small></label>
|
|
@@ -1546,7 +1551,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1546
1551
|
});
|
|
1547
1552
|
$('#install')?.addEventListener('click', () => {
|
|
1548
1553
|
state.installModalOpen = true;
|
|
1549
|
-
state.installTab = document.querySelector('input[name=mode]:checked')?.value ||
|
|
1554
|
+
state.installTab = document.querySelector('input[name=mode]:checked')?.value || 'native';
|
|
1550
1555
|
const os = document.querySelector('input[name=os]:checked')?.value || state.os || state.system?.os || 'win';
|
|
1551
1556
|
const defaultDir = os === 'win' ? 'E:\\bot' : os === 'macos' ? '/Users/you/openclaw-bot' : '/home/you/openclaw-bot';
|
|
1552
1557
|
|
package/dist/web/styles.css
CHANGED
|
@@ -2060,3 +2060,4 @@ body:has(.modal-backdrop) .bottom{display:none!important}
|
|
|
2060
2060
|
.feature-switch span{width:42px;height:24px}
|
|
2061
2061
|
.feature-switch span:after{top:2px;left:2px;width:18px;height:18px}
|
|
2062
2062
|
.feature-switch input:checked + span:after{left:22px}
|
|
2063
|
+
.install-tab.is-locked{opacity:.45;cursor:not-allowed;filter:grayscale(1)}.install-tab.is-locked:hover{transform:none;border-color:var(--hair)}
|