create-openclaw-bot 5.16.0 → 5.16.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 +323 -26
- package/dist/setup/shared/common-gen.js +14 -8
- package/dist/setup/shared/docker-gen.js +48 -1
- 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.16.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.16.2-0EA5E9?style=for-the-badge" alt="Version 5.16.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.16.
|
|
40
|
+
## 🆕 What's New in v5.16.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.16.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.16.2-0EA5E9?style=for-the-badge" alt="Version 5.16.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.16.
|
|
40
|
+
## 🆕 Có gì mới trong v5.16.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.
|
|
@@ -15,7 +15,7 @@ function loadSharedModule(modulePath, globalName) {
|
|
|
15
15
|
}
|
|
16
16
|
const { buildWorkspaceFileMap, buildCronjobSkillMd, buildInfographicGeneratorSkillMd, buildInfographicGeneratorJs } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
|
|
17
17
|
const { buildOpenclawJson, buildEnvFileContent, buildExecApprovalsJson, buildZaloConnectChannelConfig } = loadSharedModule('../setup/shared/bot-config-gen.js', '__openclawBotConfig');
|
|
18
|
-
const { buildDockerArtifacts } = loadSharedModule('../setup/shared/docker-gen.js', '__openclawDockerGen');
|
|
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
20
|
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
21
|
const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
|
|
@@ -646,18 +646,73 @@ function getRealHomedir() {
|
|
|
646
646
|
return home;
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Directories to search when a bare command is not sitting next to the running node.
|
|
651
|
+
*
|
|
652
|
+
* resolveBinPath's original assumption — "the CLI lives beside the node that runs me" — holds only
|
|
653
|
+
* while the Setup UI is launched by the same node that installed openclaw globally. It breaks the
|
|
654
|
+
* moment the UI runs under the SYSTEM node while openclaw lives in an nvm prefix: `openclaw` stays a
|
|
655
|
+
* bare name, spawn's own PATH lookup finds nothing either, and every skill/plugin install dies with
|
|
656
|
+
* `spawn openclaw ENOENT`. Hit on a customer VPS 2026-08-28, right after the UI became a systemd
|
|
657
|
+
* unit whose PATH carried only the system directories — `openclaw` existed the whole time, at
|
|
658
|
+
* /root/.nvm/versions/node/v24.20.0/bin/openclaw.
|
|
659
|
+
*
|
|
660
|
+
* PATH first (so an operator's own choice still wins), then nvm's per-version bins newest first,
|
|
661
|
+
* then the usual global npm prefixes. Cached: this runs on every spawn.
|
|
662
|
+
*/
|
|
663
|
+
let extraBinDirsCache = null;
|
|
664
|
+
function extraBinDirs() {
|
|
665
|
+
if (extraBinDirsCache) return extraBinDirsCache;
|
|
666
|
+
const dirs = String(process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
|
|
667
|
+
const home = os.homedir();
|
|
668
|
+
const nvmRoot = process.env.NVM_DIR ? join(process.env.NVM_DIR, 'versions', 'node') : join(home, '.nvm', 'versions', 'node');
|
|
669
|
+
// Newest version first: a box with several nvm nodes almost always installed the CLI under the
|
|
670
|
+
// latest one, and picking the oldest would run openclaw on a node its engines field rejects.
|
|
671
|
+
try {
|
|
672
|
+
for (const v of fs.readdirSync(nvmRoot).sort().reverse()) dirs.push(join(nvmRoot, v, 'bin'));
|
|
673
|
+
} catch (e) {}
|
|
674
|
+
for (const d of ['/usr/local/bin', '/usr/bin', '/opt/homebrew/bin', join(home, '.npm-global', 'bin'), join(home, '.local', 'bin')]) dirs.push(d);
|
|
675
|
+
extraBinDirsCache = dirs;
|
|
676
|
+
return dirs;
|
|
677
|
+
}
|
|
678
|
+
|
|
649
679
|
function resolveBinPath(cmd) {
|
|
650
680
|
if (!cmd || cmd.includes('/') || cmd.includes('\\')) return cmd;
|
|
681
|
+
const names = process.platform === 'win32' ? [`${cmd}.cmd`, `${cmd}.exe`, cmd] : [cmd];
|
|
651
682
|
const nodeBinDir = dirname(process.argv[0]);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
683
|
+
for (const name of names) {
|
|
684
|
+
const p = join(nodeBinDir, name);
|
|
685
|
+
if (existsSync(p)) return p;
|
|
686
|
+
}
|
|
656
687
|
const nodeModulesBin = join(process.cwd(), 'node_modules', '.bin', process.platform === 'win32' ? `${cmd}.cmd` : cmd);
|
|
657
688
|
if (existsSync(nodeModulesBin)) return nodeModulesBin;
|
|
689
|
+
for (const dir of extraBinDirs()) {
|
|
690
|
+
for (const name of names) {
|
|
691
|
+
const p = join(dir, name);
|
|
692
|
+
if (existsSync(p)) return p;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
658
695
|
return cmd;
|
|
659
696
|
}
|
|
660
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Child env for a resolved binary, with the binary's own directory on PATH.
|
|
700
|
+
*
|
|
701
|
+
* Load-bearing for anything picked out of an nvm prefix: `openclaw` there is a shebang script
|
|
702
|
+
* (`#!/usr/bin/env node`), so running it by absolute path still resolves `node` from the CHILD's
|
|
703
|
+
* PATH. Without this the CLI is found and then executed by whatever node happens to be first —
|
|
704
|
+
* the system one — which is not the node it was installed for.
|
|
705
|
+
*/
|
|
706
|
+
function binEnv(bin, extra = {}) {
|
|
707
|
+
const env = { ...process.env, ...extra };
|
|
708
|
+
if (!bin.includes('/') && !bin.includes('\\')) return env;
|
|
709
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
710
|
+
const dir = dirname(bin);
|
|
711
|
+
const cur = String(env.PATH || '').split(sep).filter(Boolean);
|
|
712
|
+
if (!cur.includes(dir)) env.PATH = [dir, ...cur].join(sep);
|
|
713
|
+
return env;
|
|
714
|
+
}
|
|
715
|
+
|
|
661
716
|
// Blacklist of Windows system/large directories that should never be walked
|
|
662
717
|
const SYSTEM_DIR_BLACKLIST = new Set([
|
|
663
718
|
'windows', 'program files', 'program files (x86)', 'programdata',
|
|
@@ -704,7 +759,7 @@ function run(cmd, args, opts = {}) {
|
|
|
704
759
|
const shell = process.platform === 'win32';
|
|
705
760
|
const rawBin = resolveBinPath(cmd);
|
|
706
761
|
const bin = shell && rawBin.includes(' ') && !rawBin.startsWith('"') ? `"${rawBin}"` : rawBin;
|
|
707
|
-
const child = spawn(bin, args, { cwd: opts.cwd, shell, env:
|
|
762
|
+
const child = spawn(bin, args, { cwd: opts.cwd, shell, env: binEnv(rawBin, opts.env) });
|
|
708
763
|
let stdout = '';
|
|
709
764
|
let resolved = false;
|
|
710
765
|
child.stdout.on('data', (d) => {
|
|
@@ -877,7 +932,7 @@ function runStreamed(cmd, args, opts = {}) {
|
|
|
877
932
|
cwd: opts.cwd,
|
|
878
933
|
shell,
|
|
879
934
|
windowsHide: opts.windowsHide ?? true,
|
|
880
|
-
env:
|
|
935
|
+
env: binEnv(rawBin, opts.env),
|
|
881
936
|
});
|
|
882
937
|
child.stdout.on('data', (d) => sendLog(String(d).trimEnd()));
|
|
883
938
|
child.stderr.on('data', (d) => sendLog(String(d).trimEnd()));
|
|
@@ -895,7 +950,7 @@ function runStreamedToLogFile(cmd, args, logFile, opts = {}) {
|
|
|
895
950
|
cwd: opts.cwd,
|
|
896
951
|
shell,
|
|
897
952
|
windowsHide: opts.windowsHide ?? true,
|
|
898
|
-
env:
|
|
953
|
+
env: binEnv(rawBin, opts.env),
|
|
899
954
|
});
|
|
900
955
|
let offset = 0;
|
|
901
956
|
const poll = setInterval(async () => {
|
|
@@ -923,7 +978,7 @@ function runCapture(cmd, args, opts = {}) {
|
|
|
923
978
|
cwd: opts.cwd,
|
|
924
979
|
shell,
|
|
925
980
|
windowsHide: opts.windowsHide ?? true,
|
|
926
|
-
env:
|
|
981
|
+
env: binEnv(rawBin, opts.env),
|
|
927
982
|
});
|
|
928
983
|
// Some callers need to feed stdin (pbcopy/xclip take the clipboard text that way).
|
|
929
984
|
if (opts.input != null) {
|
|
@@ -1605,6 +1660,26 @@ function validateOpenclawConfig(cfg) {
|
|
|
1605
1660
|
}
|
|
1606
1661
|
if (!cfg.channels || typeof cfg.channels !== 'object') throw httpError(500, 'openclaw.json missing channels');
|
|
1607
1662
|
|
|
1663
|
+
// Self-healing: two zalo-connect bindings can never share one account — one Zalo number, one
|
|
1664
|
+
// listener. An older edit path hardcoded `accountId: 'default'`, so an edited bot could end up
|
|
1665
|
+
// squatting the first bot's account while its own account sat unbound (no listener, and the
|
|
1666
|
+
// other bot answered in its place). Only repair where the intent is unambiguous: the agent
|
|
1667
|
+
// already has an account of its own provisioned under its agentId.
|
|
1668
|
+
const zaloAccounts = cfg.channels?.['zalo-connect']?.accounts;
|
|
1669
|
+
if (zaloAccounts) {
|
|
1670
|
+
const owner = new Map();
|
|
1671
|
+
for (const binding of cfg.bindings || []) {
|
|
1672
|
+
if (binding.match?.channel !== 'zalo-connect') continue;
|
|
1673
|
+
const accountId = binding.match?.accountId || 'default';
|
|
1674
|
+
const agentId = binding.agentId;
|
|
1675
|
+
if (!owner.has(accountId)) { owner.set(accountId, agentId); continue; }
|
|
1676
|
+
if (owner.get(accountId) === agentId || !zaloAccounts[agentId]) continue;
|
|
1677
|
+
binding.match.accountId = agentId;
|
|
1678
|
+
owner.set(agentId, agentId);
|
|
1679
|
+
sendLog(`[config] Bot "${agentId}" đang dùng chung account Zalo "${accountId}" với "${owner.get(accountId)}" — đã trả về account riêng "${agentId}".`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1608
1683
|
// Self-healing: Garbage collect any orphaned telegram accounts that are no longer bound to any active agent
|
|
1609
1684
|
if (cfg.channels?.telegram?.accounts) {
|
|
1610
1685
|
const boundAccountIds = new Set(
|
|
@@ -2012,9 +2087,15 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
2012
2087
|
agent.workspace = `/home/node/project/.openclaw/${workspaceDir}`;
|
|
2013
2088
|
agent.agentDir = `agents/${agentId}/agent`;
|
|
2014
2089
|
|
|
2015
|
-
//
|
|
2016
|
-
|
|
2017
|
-
|
|
2090
|
+
// Capture this agent's current accountId per channel BEFORE its bindings are dropped.
|
|
2091
|
+
// The zalo-connect branch below used to hardcode 'default' (and this lookup only covered
|
|
2092
|
+
// telegram), so editing ANY Zalo bot re-pointed it at the FIRST bot's Zalo account: two
|
|
2093
|
+
// bindings claimed `default`, the earlier one won, and the edited bot's own account was left
|
|
2094
|
+
// with no binding at all — so its listener never started and messages were answered by the
|
|
2095
|
+
// other bot. Seen live on a 5-bot install (bot answered under the first bot's name).
|
|
2096
|
+
const previousBindings = (cfg.bindings || []).filter((b) => b.agentId === agentId);
|
|
2097
|
+
const previousAccountId = (ch) => previousBindings.find((b) => b.match?.channel === ch)?.match?.accountId || null;
|
|
2098
|
+
const existingAccountId = previousAccountId('telegram');
|
|
2018
2099
|
cfg.bindings = (cfg.bindings || []).filter((b) => b.agentId !== agentId);
|
|
2019
2100
|
if (channel === 'telegram') {
|
|
2020
2101
|
ensureTelegramChannel(cfg);
|
|
@@ -2030,7 +2111,16 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
2030
2111
|
cfg.bindings.push({ agentId, match: { channel: 'telegram', accountId } });
|
|
2031
2112
|
} else if (channel === 'zalo-personal') {
|
|
2032
2113
|
ensureZaloConnectChannel(cfg);
|
|
2033
|
-
|
|
2114
|
+
const zc = cfg.channels['zalo-connect'];
|
|
2115
|
+
zc.accounts = zc.accounts || {};
|
|
2116
|
+
// Keep the account this bot already owns; only a bot that never had one falls back to the
|
|
2117
|
+
// create-path rule (first Zalo bot takes `default`, the rest get their own agentId account).
|
|
2118
|
+
// This agent's bindings are already filtered out above, so what remains belongs to others.
|
|
2119
|
+
const otherZaloBindings = cfg.bindings.filter((b) => b.match?.channel === 'zalo-connect').length;
|
|
2120
|
+
const accountId = previousAccountId('zalo-connect') || (otherZaloBindings === 0 ? 'default' : agentId);
|
|
2121
|
+
zc.accounts[accountId] = zc.accounts[accountId] || { enabled: true };
|
|
2122
|
+
zc.defaultAccount = zc.defaultAccount || 'default';
|
|
2123
|
+
cfg.bindings.push({ agentId, match: { channel: 'zalo-connect', accountId } });
|
|
2034
2124
|
} else if (channel === 'fb-messenger') {
|
|
2035
2125
|
ensureFbMessengerChannel(cfg, String(body.pageId || '').trim(), String(body.appId || '').trim());
|
|
2036
2126
|
cfg.bindings.push({ agentId, match: { channel: 'fb-messenger', accountId: 'default' } });
|
|
@@ -2640,6 +2730,11 @@ const NATIVE_MARKER = 'native.json';
|
|
|
2640
2730
|
// handles coexistence by asking the host what is actually taken, which the fixed offset never did.
|
|
2641
2731
|
const NATIVE_DEFAULT_GATEWAY_PORT = 18789;
|
|
2642
2732
|
const NATIVE_DEFAULT_ROUTER_PORT = 20128;
|
|
2733
|
+
// The gateway's startup migrations hold a lease on the state directory for FIVE MINUTES, and a boot
|
|
2734
|
+
// that collides with it exits immediately instead of waiting. Any health wait shorter than that
|
|
2735
|
+
// lease reports failure on a gateway that was always going to come up on its own — the old 180s
|
|
2736
|
+
// could not outlast it even in the best case.
|
|
2737
|
+
const NATIVE_GATEWAY_HEALTH_TIMEOUT_MS = 420000;
|
|
2643
2738
|
|
|
2644
2739
|
function nativeMarkerPath(projectDir) {
|
|
2645
2740
|
return join(projectDir || state.projectDir || '', '.openclaw', NATIVE_MARKER);
|
|
@@ -2717,13 +2812,22 @@ async function waitForNativeGatewayHealthy(projectDir, timeoutMs = 120000) {
|
|
|
2717
2812
|
const port = String(meta.gatewayPort || state.gatewayPort || NATIVE_DEFAULT_GATEWAY_PORT);
|
|
2718
2813
|
const started = Date.now();
|
|
2719
2814
|
let attempts = 0;
|
|
2815
|
+
let diagnosed = false;
|
|
2720
2816
|
while (Date.now() - started < timeoutMs) {
|
|
2721
2817
|
if (await probeHttpOk(`http://127.0.0.1:${port}/health`, 2500)) return true;
|
|
2722
2818
|
attempts++;
|
|
2723
2819
|
if (attempts % 5 === 0) sendLog(`[native] Waiting for gateway on ${port}... (${Math.round((Date.now() - started) / 1000)}s)`);
|
|
2820
|
+
// Half a minute of silence is already abnormal for a gateway that is merely slow, and the two
|
|
2821
|
+
// causes that never resolve on their own (a stolen port, an abandoned unit) look exactly like
|
|
2822
|
+
// "still booting" from here. Say what is wrong once, early, and keep waiting either way.
|
|
2823
|
+
if (attempts === 10 && !diagnosed) {
|
|
2824
|
+
diagnosed = true;
|
|
2825
|
+
await reportNativeGatewayBlockage(projectDir, port).catch(() => {});
|
|
2826
|
+
}
|
|
2724
2827
|
await new Promise((r) => setTimeout(r, 3000));
|
|
2725
2828
|
}
|
|
2726
2829
|
sendLog(`[native] Gateway did not answer /health on ${port} within ${Math.round(timeoutMs / 1000)}s.`);
|
|
2830
|
+
if (!diagnosed) await reportNativeGatewayBlockage(projectDir, port).catch(() => {});
|
|
2727
2831
|
return false;
|
|
2728
2832
|
}
|
|
2729
2833
|
|
|
@@ -2766,11 +2870,33 @@ async function ocDaemon(projectDir, verb, extraArgs = []) {
|
|
|
2766
2870
|
* perfectly healthy gateway reports "restart failed" — which used to send callers down a pointless
|
|
2767
2871
|
* stop+start that raced the migration lease all over again.
|
|
2768
2872
|
*/
|
|
2873
|
+
/**
|
|
2874
|
+
* Docker projects replay the idempotent config migrations on every container start — the
|
|
2875
|
+
* entrypoint embeds contextDefaultsScript. Native projects have no entrypoint, so an existing
|
|
2876
|
+
* native bot never received those fixes (a bot created before 5.16.0 kept the smart-route
|
|
2877
|
+
* contextWindow at 200000 and stayed exposed to the compaction deadlock). Replaying the exact
|
|
2878
|
+
* same script here, right before every gateway (re)start, gives native the docker semantics:
|
|
2879
|
+
* migrate first, then boot on the upgraded config.
|
|
2880
|
+
*/
|
|
2881
|
+
async function runNativeConfigMigrations(projectDir) {
|
|
2882
|
+
const res = await runCapture(process.execPath, ['-e', contextDefaultsScript], {
|
|
2883
|
+
cwd: projectDir,
|
|
2884
|
+
env: nativeEnv(projectDir),
|
|
2885
|
+
shell: false,
|
|
2886
|
+
});
|
|
2887
|
+
if (res.code !== 0) sendLog(`[native] config migrations skipped: ${String(res.stderr || res.stdout || '').trim().slice(0, 200)}`);
|
|
2888
|
+
return res.code === 0;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2769
2891
|
async function restartNativeRuntime(projectDir) {
|
|
2770
|
-
// Every restart is a chance to repair a project installed before these
|
|
2771
|
-
// calls are no-ops once the service env is complete
|
|
2892
|
+
// Every restart is a chance to repair a project installed before these fixes existed — the
|
|
2893
|
+
// calls are no-ops once the service env is complete, stray files are adopted, and the config
|
|
2894
|
+
// already carries the migrated defaults.
|
|
2772
2895
|
await adoptStrayNativeHome(projectDir).catch(() => {});
|
|
2773
2896
|
await syncNativeServiceEnv(projectDir).catch(() => {});
|
|
2897
|
+
await runNativeConfigMigrations(projectDir).catch(() => {});
|
|
2898
|
+
await hardenNativeServiceRestarts(projectDir).catch(() => {});
|
|
2899
|
+
await clearNativeServiceFailure(projectDir).catch(() => {});
|
|
2774
2900
|
const stopStart = async () => {
|
|
2775
2901
|
await ocDaemon(projectDir, 'stop');
|
|
2776
2902
|
return ocDaemon(projectDir, 'start');
|
|
@@ -2795,9 +2921,10 @@ async function restartNativeRuntime(projectDir) {
|
|
|
2795
2921
|
res = await ocDaemon(projectDir, 'restart');
|
|
2796
2922
|
if (res.code !== 0) res = await stopStart();
|
|
2797
2923
|
}
|
|
2798
|
-
//
|
|
2799
|
-
//
|
|
2800
|
-
|
|
2924
|
+
// With the start limit lifted (hardenNativeServiceRestarts) systemd really does keep restarting a
|
|
2925
|
+
// crash-looping unit every RestartSec, so a gateway blocked by a lease we never saw still comes up
|
|
2926
|
+
// on its own — give it the full lease window before calling the restart a failure.
|
|
2927
|
+
if (!(await waitForNativeGatewayHealthy(projectDir, NATIVE_GATEWAY_HEALTH_TIMEOUT_MS))) {
|
|
2801
2928
|
throw new Error('gateway did not answer /health after restart');
|
|
2802
2929
|
}
|
|
2803
2930
|
return true;
|
|
@@ -2924,6 +3051,112 @@ async function ensureSystemdLinger() {
|
|
|
2924
3051
|
return false;
|
|
2925
3052
|
}
|
|
2926
3053
|
|
|
3054
|
+
/**
|
|
3055
|
+
* Stop systemd abandoning the gateway while the state lease is still held.
|
|
3056
|
+
*
|
|
3057
|
+
* `openclaw daemon install` writes `StartLimitBurst=5` / `StartLimitIntervalSec=60` next to
|
|
3058
|
+
* `Restart=always`. The gateway's startup migrations take a lease on the state directory that lasts
|
|
3059
|
+
* five minutes, and a boot that collides with it exits 1 *immediately* — so five collisions burn the
|
|
3060
|
+
* whole limit in 30 seconds, systemd logs "Start request repeated too quickly", and the unit stays
|
|
3061
|
+
* dead for good even though the lease frees itself minutes later. Verified on a fresh Ubuntu 24.04
|
|
3062
|
+
* VPS (2026-08-28): the setup UI sat on "Waiting for gateway on 18789..." until it timed out, while
|
|
3063
|
+
* `systemctl --user start` afterwards brought the same unit up on the first try.
|
|
3064
|
+
*
|
|
3065
|
+
* A drop-in rather than an edit to the unit body: `daemon install --force` rewrites the unit on every
|
|
3066
|
+
* run, and drop-ins survive that.
|
|
3067
|
+
*/
|
|
3068
|
+
async function hardenNativeServiceRestarts(projectDir) {
|
|
3069
|
+
if (process.platform !== 'linux' || !isNativeProject(projectDir)) return false;
|
|
3070
|
+
const unit = `${nativeServiceLabel(projectDir)}.service`;
|
|
3071
|
+
const dir = join(os.homedir(), '.config', 'systemd', 'user', `${unit}.d`);
|
|
3072
|
+
const file = join(dir, '99-openclaw-setup.conf');
|
|
3073
|
+
const body = [
|
|
3074
|
+
'# Written by openclaw-setup — regenerated on every install, do not edit.',
|
|
3075
|
+
'# Startup migrations hold the state lease for ~5 minutes and a colliding boot exits at once, so',
|
|
3076
|
+
'# the stock StartLimitBurst=5/StartLimitIntervalSec=60 abandons the unit for good within 30s.',
|
|
3077
|
+
'# No start limit = systemd keeps retrying every RestartSec until the lease frees itself.',
|
|
3078
|
+
'[Unit]',
|
|
3079
|
+
'StartLimitIntervalSec=0',
|
|
3080
|
+
'[Service]',
|
|
3081
|
+
'RestartSec=10',
|
|
3082
|
+
'',
|
|
3083
|
+
].join('\n');
|
|
3084
|
+
if ((await fsp.readFile(file, 'utf8').catch(() => '')) === body) return true;
|
|
3085
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
3086
|
+
await fsp.writeFile(file, body, 'utf8');
|
|
3087
|
+
await run('systemctl', ['--user', 'daemon-reload'], {}).catch(() => {});
|
|
3088
|
+
sendLog('[native] service hardened: systemd start limit lifted (a migration lease can take 5 min).');
|
|
3089
|
+
return true;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
/**
|
|
3093
|
+
* A unit parked at "Start request repeated too quickly" refuses every later `start` until its
|
|
3094
|
+
* failure is cleared — including the one that would finally succeed. Cheap and idempotent, so it
|
|
3095
|
+
* runs before each start and also repairs a project abandoned by an earlier install.
|
|
3096
|
+
*/
|
|
3097
|
+
async function clearNativeServiceFailure(projectDir) {
|
|
3098
|
+
if (process.platform !== 'linux' || !isNativeProject(projectDir)) return false;
|
|
3099
|
+
const unit = `${nativeServiceLabel(projectDir)}.service`;
|
|
3100
|
+
const shown = await runCapture('systemctl', ['--user', 'show', '-p', 'ActiveState', '--value', unit], { shell: false, timeout: 10000 });
|
|
3101
|
+
if (!/^failed/i.test(String(shown.stdout || '').trim())) return false;
|
|
3102
|
+
await run('systemctl', ['--user', 'reset-failed', unit], {}).catch(() => {});
|
|
3103
|
+
sendLog(`[native] cleared the previous failed state of ${unit} before starting it.`);
|
|
3104
|
+
return true;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* Name whatever already owns a port, so a stalled wait can say why it will never end.
|
|
3109
|
+
*
|
|
3110
|
+
* The trap this exists for: an operator runs the port-forward command out of `ssh <host>-setup`
|
|
3111
|
+
* (`ssh -L 18789:127.0.0.1:18789 root@<host>`) while already logged INTO that host instead of from
|
|
3112
|
+
* their own machine. ssh binds 18789 locally and forwards it to 18789 on the same box — a self-loop
|
|
3113
|
+
* that accepts every connection and answers nothing. The gateway can then never bind its port, and
|
|
3114
|
+
* health probes hang instead of failing, so the port looks alive and is useless. Seen on a customer
|
|
3115
|
+
* VPS 2026-08-28. `ss` (iproute2) is used on Linux rather than `lsof`, which a minimal Ubuntu lacks.
|
|
3116
|
+
*/
|
|
3117
|
+
async function describePortHolder(port) {
|
|
3118
|
+
const trim = (v) => String(v || '').trim();
|
|
3119
|
+
if (process.platform === 'win32') {
|
|
3120
|
+
const out = await runCapture('powershell', ['-NoProfile', '-Command', `$p=(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess; if ($p) { "pid $p: " + (Get-Process -Id $p -ErrorAction SilentlyContinue).Path }`], { shell: false, timeout: 15000 });
|
|
3121
|
+
return trim(out.stdout);
|
|
3122
|
+
}
|
|
3123
|
+
if (process.platform === 'linux') {
|
|
3124
|
+
const ss = await runCapture('ss', ['-ltnp'], { shell: false, timeout: 10000 });
|
|
3125
|
+
const line = String(ss.stdout || '').split('\n').find((l) => new RegExp(`[:.]${port}\\s`).test(l)) || '';
|
|
3126
|
+
const pid = (line.match(/pid=(\d+)/) || [])[1];
|
|
3127
|
+
if (!pid) return trim(line);
|
|
3128
|
+
const ps = await runCapture('ps', ['-o', 'args=', '-p', pid], { shell: false, timeout: 10000 });
|
|
3129
|
+
return `pid ${pid}: ${trim(ps.stdout) || trim(line)}`;
|
|
3130
|
+
}
|
|
3131
|
+
const out = await runCapture('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN'], { shell: false, timeout: 10000 });
|
|
3132
|
+
return trim(String(out.stdout || '').split('\n')[1]);
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* Explain a gateway that is not answering, instead of counting down in silence.
|
|
3137
|
+
*
|
|
3138
|
+
* Three causes, all indistinguishable from "still booting" without this: an ssh port-forward
|
|
3139
|
+
* self-loop holding the port (describePortHolder), the migration lease plus systemd's start limit
|
|
3140
|
+
* leaving the unit dead for good (hardenNativeServiceRestarts), or an ordinary crash on boot — for
|
|
3141
|
+
* which the unit's own journal is the only thing that ever says so.
|
|
3142
|
+
*/
|
|
3143
|
+
async function reportNativeGatewayBlockage(projectDir, port) {
|
|
3144
|
+
const holder = await describePortHolder(port).catch(() => '');
|
|
3145
|
+
if (holder) {
|
|
3146
|
+
sendLog(`[native] Port ${port} is already held by → ${holder}`);
|
|
3147
|
+
if (/(^|\/|\s)ssh(\s|$)/.test(holder) && new RegExp(`-L\\s*\\d*:?${port}:`).test(holder)) {
|
|
3148
|
+
sendLog(`[native] That is an SSH port-forward, not the gateway. \`ssh -L ${port}:...\` belongs on YOUR OWN machine — run it on the server and it steals port ${port} from the gateway and answers nothing. Kill that ssh process on the server, then start the gateway again.`);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
if (process.platform !== 'linux' || !isNativeProject(projectDir)) return;
|
|
3152
|
+
const unit = `${nativeServiceLabel(projectDir)}.service`;
|
|
3153
|
+
const st = await runCapture('systemctl', ['--user', 'show', '-p', 'ActiveState', '-p', 'SubState', '-p', 'NRestarts', '--value', unit], { shell: false, timeout: 10000 }).catch(() => ({}));
|
|
3154
|
+
const vals = String(st.stdout || '').split('\n').map((v) => v.trim()).filter(Boolean);
|
|
3155
|
+
if (vals.length) sendLog(`[native] ${unit} → ${vals.join(' / ')}`);
|
|
3156
|
+
const log = await runCapture('journalctl', ['--user', '-u', unit, '-n', '15', '--no-pager'], { shell: false, timeout: 15000 }).catch(() => ({}));
|
|
3157
|
+
for (const line of String(log.stdout || '').split('\n').map((l) => l.trimEnd()).filter(Boolean)) sendLog(`[native] ${line}`);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
2927
3160
|
/**
|
|
2928
3161
|
* Native counterpart of the docker entrypoint's `ensure_plugin` (docker-gen.js).
|
|
2929
3162
|
*
|
|
@@ -2983,7 +3216,7 @@ function startDetached(cmd, args, opts = {}) {
|
|
|
2983
3216
|
detached: true,
|
|
2984
3217
|
stdio: 'ignore',
|
|
2985
3218
|
windowsHide: opts.windowsHide ?? true,
|
|
2986
|
-
env:
|
|
3219
|
+
env: binEnv(rawBin, opts.env),
|
|
2987
3220
|
});
|
|
2988
3221
|
child.on('error', (err) => sendLog(`[native] Failed to start "${cmd}": ${err.message}`));
|
|
2989
3222
|
child.unref();
|
|
@@ -3077,20 +3310,38 @@ async function startNativeRuntime({ projectDir, osChoice = '', gatewayPort, rout
|
|
|
3077
3310
|
// straight away, needs no follow-up restart, and prints no "plugin not found" warnings.
|
|
3078
3311
|
await ensureNativePlugins(projectDir).catch((e) => sendLog(`[native] plugin bootstrap skipped: ${e.message}`));
|
|
3079
3312
|
|
|
3313
|
+
// Config migrations BEFORE the first boot too — same ordering the container entrypoint uses
|
|
3314
|
+
// (migrate, then start), so an adopted pre-existing project boots on the upgraded config.
|
|
3315
|
+
await runNativeConfigMigrations(projectDir).catch(() => {});
|
|
3316
|
+
|
|
3080
3317
|
// Managed service = auto-restart (KeepAlive/Restart=always) and start-at-login, the native
|
|
3081
3318
|
// equivalent of docker's `restart: always`. --force so re-running install updates the port.
|
|
3082
3319
|
const env = nativeEnv(projectDir);
|
|
3083
3320
|
await ensureSystemdLinger();
|
|
3084
3321
|
await run('openclaw', ['daemon', 'install', '--force', '--port', String(gwPort)], { cwd: projectDir, env });
|
|
3322
|
+
await hardenNativeServiceRestarts(projectDir).catch((e) => sendLog(`[native] service hardening skipped: ${e.message}`));
|
|
3323
|
+
await clearNativeServiceFailure(projectDir).catch(() => {});
|
|
3324
|
+
// `daemon install` already STARTED the unit, and that first boot begins the state migrations that
|
|
3325
|
+
// hold a five-minute lease on the state directory. This used to restart it right away — to pick up
|
|
3326
|
+
// the env completed below — and the restart landed mid-migration: the lease was left behind, every
|
|
3327
|
+
// following boot exited 1 until it expired, and systemd's start limit abandoned the unit long
|
|
3328
|
+
// before that. The setup UI then waited out its whole timeout on a service that was never coming
|
|
3329
|
+
// back (real Ubuntu 24.04 VPS, 2026-08-28). So let this boot finish UNDISTURBED first, and only
|
|
3330
|
+
// then repair the env and restart a settled gateway.
|
|
3331
|
+
const firstBootOk = await waitForNativeGatewayHealthy(projectDir, NATIVE_GATEWAY_HEALTH_TIMEOUT_MS);
|
|
3085
3332
|
// Order is load-bearing: adopt the stray files FIRST, then complete the service env. The other way
|
|
3086
3333
|
// round, the gateway boots with a corrected OPENCLAW_HOME, finds no Zalo session there, and asks
|
|
3087
3334
|
// for a new QR scan even though a perfectly good session exists in the home directory.
|
|
3088
3335
|
await adoptStrayNativeHome(projectDir).catch((e) => sendLog(`[migrate] stray home skipped: ${e.message}`));
|
|
3089
|
-
await syncNativeServiceEnv(projectDir).catch((e) => sendLog(`[native] service env sync skipped: ${e.message}`));
|
|
3090
|
-
|
|
3091
|
-
//
|
|
3092
|
-
|
|
3093
|
-
|
|
3336
|
+
const envAdded = await syncNativeServiceEnv(projectDir).catch((e) => { sendLog(`[native] service env sync skipped: ${e.message}`); return []; });
|
|
3337
|
+
// One restart, and only when there is something to gain by it: a completed env to load, or a first
|
|
3338
|
+
// boot that never answered (in which case the restart is the repair attempt).
|
|
3339
|
+
if (envAdded.length || !firstBootOk) {
|
|
3340
|
+
sendLog(envAdded.length
|
|
3341
|
+
? '[native] restarting gateway to load the completed service env'
|
|
3342
|
+
: '[native] first boot never answered /health — restarting once to recover');
|
|
3343
|
+
await restartNativeRuntime(projectDir).catch((e) => sendLog(`[native] restart after install: ${e.message}`));
|
|
3344
|
+
}
|
|
3094
3345
|
sendLog(`[native] gateway service "${label}" running on 127.0.0.1:${gwPort}, 9router on 127.0.0.1:${rtPort}`);
|
|
3095
3346
|
return { gatewayPort: gwPort, routerPort: rtPort, label };
|
|
3096
3347
|
}
|
|
@@ -3243,7 +3494,7 @@ async function recreateDockerBot(projectDir) {
|
|
|
3243
3494
|
// under a state lease, a restart mid-migration exits 1, and systemd's start limit can then
|
|
3244
3495
|
// abandon the unit. This is the same trap the docker path avoids by waiting for the container
|
|
3245
3496
|
// before touching it (see startZaloConnectLogin) — wait for /health first.
|
|
3246
|
-
await waitForNativeGatewayHealthy(projectDir,
|
|
3497
|
+
await waitForNativeGatewayHealthy(projectDir, NATIVE_GATEWAY_HEALTH_TIMEOUT_MS);
|
|
3247
3498
|
// The bot that was just created/edited may have added the Zalo channel or the context engine to
|
|
3248
3499
|
// openclaw.json; put those plugins on disk now so this one reload loads them too.
|
|
3249
3500
|
await ensureNativePlugins(projectDir).catch((e) => sendLog(`[native] plugin ensure skipped: ${e.message}`));
|
|
@@ -6743,11 +6994,57 @@ async function printRemoteAccessHint(uiPort) {
|
|
|
6743
6994
|
console.log(` ssh ${fwd} ${sshUserName()}@${ip}`);
|
|
6744
6995
|
console.log(` then open: http://localhost:${uiPort}`);
|
|
6745
6996
|
console.log(` (forwards ${ports.length} live port${ports.length > 1 ? 's' : ''}: ${ports.join(', ')})`);
|
|
6997
|
+
// Spelled out because the obvious mistake is to paste that line into the shell it was printed in.
|
|
6998
|
+
// Run there, ssh binds every one of those ports on THIS box and forwards them straight back to
|
|
6999
|
+
// itself: the gateway can no longer bind 18789, and the self-loop accepts connections while
|
|
7000
|
+
// answering nothing, so the port looks alive and the whole install hangs on "Waiting for gateway".
|
|
7001
|
+
// Cost a customer VPS install on 2026-08-28.
|
|
7002
|
+
console.log(' ⚠️ Run that line on YOUR OWN machine — NOT in this shell. On the server it');
|
|
7003
|
+
console.log(` steals port ${ports.join('/')} from the bot and the install never finishes.`);
|
|
6746
7004
|
console.log('');
|
|
6747
7005
|
}
|
|
6748
7006
|
|
|
7007
|
+
/** True when whatever answers on host:port already looks like an OpenClaw Setup UI. */
|
|
7008
|
+
async function detectExistingSetupUi(host, port) {
|
|
7009
|
+
try {
|
|
7010
|
+
const res = await fetch(`http://${host}:${port}/`, { signal: AbortSignal.timeout(2500) });
|
|
7011
|
+
if (!res.ok) return false;
|
|
7012
|
+
const body = (await res.text()).slice(0, 65536);
|
|
7013
|
+
return /openclaw/i.test(body);
|
|
7014
|
+
} catch (_) {
|
|
7015
|
+
return false;
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
|
|
6749
7019
|
export async function startLocalInstaller({ host = '127.0.0.1', preferredPort = 51789, openBrowser = true, projectDir = process.cwd() } = {}) {
|
|
6750
7020
|
const port = await findPort(host, preferredPort);
|
|
7021
|
+
if (port !== preferredPort && (await detectExistingSetupUi(host, preferredPort))) {
|
|
7022
|
+
// Another Setup UI already owns the preferred port (a systemd service, an earlier npx run…).
|
|
7023
|
+
// Hopping to :51790 here is exactly how operators end up with SSH tunnels and printed hints
|
|
7024
|
+
// pointing at a port nothing serves — so reuse the running instance instead of starting a
|
|
7025
|
+
// second one, and keep this process alive so an `ssh -L … "npx create-openclaw-bot"`
|
|
7026
|
+
// one-liner still holds the tunnel open. If the other instance ever goes away, take the
|
|
7027
|
+
// port over so the URL keeps working without the operator re-running anything.
|
|
7028
|
+
const url = `http://${host}:${preferredPort}`;
|
|
7029
|
+
activeUiHost = host;
|
|
7030
|
+
activeUiPort = preferredPort;
|
|
7031
|
+
activeUiProjectDir = projectDir;
|
|
7032
|
+
console.log(`OpenClaw Setup UI is already running: ${url}`);
|
|
7033
|
+
console.log('Reusing the running instance — nothing new was started. Keep this window open if it holds your SSH tunnel.');
|
|
7034
|
+
ensureReopenShortcut();
|
|
7035
|
+
if (openBrowser) openUrl(url);
|
|
7036
|
+
printRemoteAccessHint(preferredPort).catch(() => {});
|
|
7037
|
+
const takeover = setInterval(async () => {
|
|
7038
|
+
if ((await findPort(host, preferredPort)) !== preferredPort) return; // still busy
|
|
7039
|
+
clearInterval(takeover);
|
|
7040
|
+
console.log(`Port ${preferredPort} freed up — starting a Setup UI there to keep ${url} working.`);
|
|
7041
|
+
startLocalInstaller({ host, preferredPort, openBrowser: false, projectDir }).catch(() => {});
|
|
7042
|
+
}, 5000);
|
|
7043
|
+
return;
|
|
7044
|
+
}
|
|
7045
|
+
if (port !== preferredPort) {
|
|
7046
|
+
console.log(`⚠ Port ${preferredPort} is busy with something that is not a Setup UI — using ${port} instead.`);
|
|
7047
|
+
}
|
|
6751
7048
|
activeUiHost = host;
|
|
6752
7049
|
activeUiPort = port;
|
|
6753
7050
|
activeUiProjectDir = projectDir;
|
|
@@ -6773,4 +7070,4 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
|
|
|
6773
7070
|
]).catch(() => {});
|
|
6774
7071
|
}
|
|
6775
7072
|
|
|
6776
|
-
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 };
|
|
7073
|
+
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 };
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
(function (root) {
|
|
3
|
-
|
|
3
|
+
// GHIM version, khong dung @latest: Dockerfile sinh ra mang spec nay, nen "@latest" nghia la
|
|
4
|
+
// MOI lan rebuild image co the nhay han mot the he openclaw — schema config doi, state DB doi
|
|
5
|
+
// migration, va bot khach chet ngay sau mot lan bam rebuild vo hai (ca that 103.98.149.154,
|
|
6
|
+
// 31/08/2026: rebuild keo 2026.7.1-2 → 2026.8.1, gateway tu choi boot, 66 lan restart).
|
|
7
|
+
// Nang version = doi spec nay MOT CACH CO Y trong mot ban phat hanh setup, kem kiem chung.
|
|
8
|
+
const OPENCLAW_NPM_SPEC = 'openclaw@2026.8.1';
|
|
4
9
|
const OPENCLAW_RUNTIME_PACKAGES = 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
|
|
5
10
|
const NINE_ROUTER_NPM_SPEC = '9router@latest';
|
|
6
11
|
const NINE_ROUTER_PORT = 20128;
|
|
@@ -268,13 +273,14 @@ If setup reported a plugin install error, run this after the bot is running:
|
|
|
268
273
|
{
|
|
269
274
|
id: 'smart-route',
|
|
270
275
|
name: 'Smart Proxy (Auto Route)',
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
276
|
+
// 1M (Kent chot 01/09/2026) — dao lai quyet dinh 131072 cua 5.16.0. Boi canh de doc
|
|
277
|
+
// lai sau: 131072 sinh ra vi smart-route fan-out sang upstream free ma model nho nhat
|
|
278
|
+
// trong pool tran ~128k; khai 200k lam phien day den muc chinh loi goi compaction cung
|
|
279
|
+
// overflow ("auto-compaction could not recover" cho den /new). Gio pool upstream da la
|
|
280
|
+
// cac model cua so lon nen khai 1M de phien dai khong bi compaction som. TRADE-OFF: neu
|
|
281
|
+
// mot combo van con model 128k thi phien vuot 128k se chet dung nhu hoi 5.15.x — do la
|
|
282
|
+
// van de cua combo, sua bang cach chon model, khong sua bang cach ha window o day nua.
|
|
283
|
+
contextWindow: 1048576,
|
|
278
284
|
maxTokens: 8192,
|
|
279
285
|
input: ['text', 'image'],
|
|
280
286
|
},
|
|
@@ -194,6 +194,21 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
194
194
|
return `node -e \\"const fs=require('fs'),os=require('os'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const gp=Number(process.env.OPENCLAW_GATEWAY_PORT||process.env.OPENCLAW_PORT)||c.gateway?.port||18789;const a=new Set(['http://localhost:'+gp,'http://127.0.0.1:'+gp,'http://0.0.0.0:'+gp]);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':'+gp);}}const p9=c.models&&c.models.providers&&c.models.providers['9router'];if(p9){p9.request=Object.assign({},p9.request,{allowPrivateNetwork:true});}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:gp,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\"`;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Idempotent config upgrade replayed on every runtime start. Docker embeds it in the container
|
|
198
|
+
// entrypoint; the native runtime (local-server.js) runs it via `node -e` before every gateway
|
|
199
|
+
// (re)start — one script, both deploy modes, so native bots stop missing config fixes like the
|
|
200
|
+
// smart-route contextWindow 200000/131072 → 1048576 (Kent chot 01/09/2026; chi dung den DUNG
|
|
201
|
+
// hai gia tri setup tung ghi, custom tuning giu nguyen). Full rationale sits with the
|
|
202
|
+
// entrypoint block inside buildDockerArtifacts.
|
|
203
|
+
// ⚠️ toolResultMaxChars: openclaw ≥2026.8 BO key nay khoi schema va TU CHOI BOOT khi thay no.
|
|
204
|
+
// Ban cu cua script nay them key vo dieu kien moi lan container start — nen mot khach rebuild
|
|
205
|
+
// image (Dockerfile keo openclaw moi) la roi vao vong: gateway chet vi key ⇒ xoa tay ⇒ entrypoint
|
|
206
|
+
// them lai ⇒ chet tiep, khong lo ra thu pham (ca that 103.98.149.154, 31/08/2026, 66 lan restart).
|
|
207
|
+
// Gio gate theo version openclaw THAT trong container: <2026.8 thi backfill nhu cu, ≥2026.8 thi
|
|
208
|
+
// GO key neu con (tu chua cac project da nhiem). Khong doc duoc version thi KHONG dong nao chay
|
|
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>=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){}}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
|
+
|
|
197
212
|
function buildDockerArtifacts(options) {
|
|
198
213
|
const {
|
|
199
214
|
openClawNpmSpec,
|
|
@@ -277,7 +292,8 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
277
292
|
// the compaction summarize call itself overflowed and only /new recovered. Only the
|
|
278
293
|
// exact setup-written 200000 is rewritten; an operator's custom value is left alone.
|
|
279
294
|
// Each key is only filled in when absent, so an operator's own tuning is never clobbered.
|
|
280
|
-
|
|
295
|
+
// (The script itself lives at module scope — the native runtime replays the exact same
|
|
296
|
+
// migration before every gateway (re)start, since native projects have no entrypoint.)
|
|
281
297
|
// Companion backfill for the same older projects: their TOOLS.md was generated before the
|
|
282
298
|
// skill-authoring / long-turn guidance existed, and workspace files are only written when a
|
|
283
299
|
// bot is created — so a rebuild alone leaves the assistant stopping at "proposal awaiting
|
|
@@ -451,6 +467,36 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
451
467
|
// `set -e` is on: a non-zero return here must never stop the gateway from starting.
|
|
452
468
|
'start_local_headless_chrome || true',
|
|
453
469
|
].join('\n'));
|
|
470
|
+
// openclaw ≥2026.8 TACH duckduckgo khoi bundle thanh plugin external co capability consent.
|
|
471
|
+
// Config do setup sinh (bot chon skill web-search) van khai `plugins.entries.duckduckgo`,
|
|
472
|
+
// va gateway 2026.8.x TU CHOI ready khi entry khai ma plugin chua cai ("Plugin verification
|
|
473
|
+
// failed … requires capability consent") — do that tren 103.98.149.154, 31/08/2026.
|
|
474
|
+
// Cai o day, truoc gateway run, dung lenh + co ma chinh gateway goi y; `yes |` cho cau hoi
|
|
475
|
+
// consent; loi khong chan boot (|| true) — thieu web search chi la mat mot tool phu.
|
|
476
|
+
runtimeParts.push([
|
|
477
|
+
'if grep -q \'"duckduckgo"\' "$OPENCLAW_HOME/openclaw.json" 2>/dev/null && [ ! -d "$OPENCLAW_HOME/extensions/duckduckgo" ]; then',
|
|
478
|
+
' echo "[entrypoint] installing external duckduckgo plugin (openclaw >=2026.8 unbundled it)"',
|
|
479
|
+
' yes | openclaw plugins install @openclaw/duckduckgo-plugin --accept-capabilities || true',
|
|
480
|
+
'fi',
|
|
481
|
+
].join('\n'));
|
|
482
|
+
// ── Doctor-on-upgrade ─────────────────────────────────────────────────────────────────
|
|
483
|
+
// Update openclaw khong bao gio chi la "doi so version": schema config doi (key cu bi TU CHOI
|
|
484
|
+
// chu khong bo qua) va state DB doi migration (audit-events-v2…), va doctor chi chay sach khi
|
|
485
|
+
// gateway DANG TAT — tuc dung ngay tai day, truoc `gateway run`, trong container vua boot.
|
|
486
|
+
// So version hien tai voi lan boot truoc (marker trong $OPENCLAW_HOME); khac nhau moi chay,
|
|
487
|
+
// hai luot vi doctor boc migration theo LOP (ca that 103.98.149.154 can nhieu luot). `yes |`
|
|
488
|
+
// vi doctor co cau hoi tuong tac; loi doctor khong chan boot — gateway se tu noi not phan con
|
|
489
|
+
// thieu, con hon container chet cung khong ai chan doan duoc.
|
|
490
|
+
runtimeParts.push([
|
|
491
|
+
'OC_VER="$(openclaw --version 2>/dev/null | head -1)"',
|
|
492
|
+
'OC_VER_MARKER="$OPENCLAW_HOME/.openclaw-last-version"',
|
|
493
|
+
'if [ -n "$OC_VER" ] && [ "$OC_VER" != "$(cat "$OC_VER_MARKER" 2>/dev/null)" ]; then',
|
|
494
|
+
' echo "[entrypoint] openclaw version changed ($(cat "$OC_VER_MARKER" 2>/dev/null || echo none) -> $OC_VER); running doctor --fix"',
|
|
495
|
+
' yes | openclaw doctor --fix || true',
|
|
496
|
+
' yes | openclaw doctor --fix || true',
|
|
497
|
+
' printf %s "$OC_VER" > "$OC_VER_MARKER" || true',
|
|
498
|
+
'fi',
|
|
499
|
+
].join('\n'));
|
|
454
500
|
runtimeParts.push('openclaw gateway run');
|
|
455
501
|
const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
|
|
456
502
|
let browserInstall = '';
|
|
@@ -697,6 +743,7 @@ ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''}
|
|
|
697
743
|
build9RouterComposeEntrypointScript,
|
|
698
744
|
buildGatewayPatchCmd,
|
|
699
745
|
buildDockerArtifacts,
|
|
746
|
+
contextDefaultsScript,
|
|
700
747
|
};
|
|
701
748
|
|
|
702
749
|
})(typeof globalThis !== 'undefined' ? globalThis : {});
|