create-openclaw-bot 5.15.4 → 5.16.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 +2 -2
- package/README.vi.md +2 -2
- package/dist/server/local-server.js +116 -1
- package/dist/setup/shared/common-gen.js +7 -1
- package/dist/setup/shared/docker-gen.js +16 -1
- package/dist/web/app.js +65 -7
- 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.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.16.0-0EA5E9?style=for-the-badge" alt="Version 5.16.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.16.0
|
|
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.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.16.0-0EA5E9?style=for-the-badge" alt="Version 5.16.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.16.0
|
|
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.
|
|
@@ -2767,6 +2767,10 @@ async function ocDaemon(projectDir, verb, extraArgs = []) {
|
|
|
2767
2767
|
* stop+start that raced the migration lease all over again.
|
|
2768
2768
|
*/
|
|
2769
2769
|
async function restartNativeRuntime(projectDir) {
|
|
2770
|
+
// Every restart is a chance to repair a project installed before these two fixes existed — both
|
|
2771
|
+
// calls are no-ops once the service env is complete and the stray files have been adopted.
|
|
2772
|
+
await adoptStrayNativeHome(projectDir).catch(() => {});
|
|
2773
|
+
await syncNativeServiceEnv(projectDir).catch(() => {});
|
|
2770
2774
|
const stopStart = async () => {
|
|
2771
2775
|
await ocDaemon(projectDir, 'stop');
|
|
2772
2776
|
return ocDaemon(projectDir, 'start');
|
|
@@ -2799,6 +2803,102 @@ async function restartNativeRuntime(projectDir) {
|
|
|
2799
2803
|
return true;
|
|
2800
2804
|
}
|
|
2801
2805
|
|
|
2806
|
+
/**
|
|
2807
|
+
* Make the generated service carry everything nativeEnv() promises.
|
|
2808
|
+
*
|
|
2809
|
+
* `openclaw daemon install` propagates only a fixed allow-list into the service it writes:
|
|
2810
|
+
* OPENCLAW_STATE_DIR survives, but OPENCLAW_HOME does NOT — verified on both a systemd user unit and
|
|
2811
|
+
* a launchd env-wrapper. Anything resolving paths from OPENCLAW_HOME then falls back to `~/.openclaw`
|
|
2812
|
+
* and writes OUTSIDE the project. zalo-connect is the visible casualty: it stages inbound files and
|
|
2813
|
+
* its Zalo session credentials under the wrong home, so a PDF sent to the bot lands somewhere the
|
|
2814
|
+
* agent's workspace cannot reach ("em chưa trích xuất được nội dung từ PDF") and the session sits in
|
|
2815
|
+
* a different home from the config that describes it.
|
|
2816
|
+
*
|
|
2817
|
+
* Idempotent: keys already present are left alone, so this is safe to run on every restart and it
|
|
2818
|
+
* self-heals projects created before the fix.
|
|
2819
|
+
*/
|
|
2820
|
+
async function syncNativeServiceEnv(projectDir) {
|
|
2821
|
+
if (!isNativeProject(projectDir)) return [];
|
|
2822
|
+
const want = nativeEnv(projectDir);
|
|
2823
|
+
const label = nativeServiceLabel(projectDir);
|
|
2824
|
+
|
|
2825
|
+
if (process.platform === 'linux') {
|
|
2826
|
+
const unit = `${label}.service`;
|
|
2827
|
+
const shown = await runCapture('systemctl', ['--user', 'show', '-p', 'FragmentPath', '--value', unit], { shell: false, timeout: 10000 });
|
|
2828
|
+
const path = String(shown.stdout || '').trim() || join(os.homedir(), '.config', 'systemd', 'user', unit);
|
|
2829
|
+
if (!existsSync(path)) return [];
|
|
2830
|
+
const lines = (await fsp.readFile(path, 'utf8')).split('\n');
|
|
2831
|
+
const have = new Set();
|
|
2832
|
+
for (const line of lines) {
|
|
2833
|
+
const m = line.trim().match(/^Environment="?([A-Z_0-9]+)=/);
|
|
2834
|
+
if (m) have.add(m[1]);
|
|
2835
|
+
}
|
|
2836
|
+
const missing = Object.entries(want).filter(([k, v]) => !have.has(k) && v !== '');
|
|
2837
|
+
if (!missing.length) return [];
|
|
2838
|
+
// Insert after the last existing Environment= line so the additions stay inside [Service].
|
|
2839
|
+
let at = -1;
|
|
2840
|
+
lines.forEach((line, i) => { if (line.startsWith('Environment=')) at = i; });
|
|
2841
|
+
if (at < 0) at = lines.findIndex((line) => line.trim() === '[Service]');
|
|
2842
|
+
if (at < 0) return [];
|
|
2843
|
+
lines.splice(at + 1, 0, ...missing.map(([k, v]) => `Environment=${k}=${v}`));
|
|
2844
|
+
await fsp.copyFile(path, `${path}.bak`).catch(() => {});
|
|
2845
|
+
await fsp.writeFile(path, lines.join('\n'), 'utf8');
|
|
2846
|
+
await run('systemctl', ['--user', 'daemon-reload'], {}).catch(() => {});
|
|
2847
|
+
sendLog(`[native] service env completed: ${missing.map(([k]) => k).join(', ')}`);
|
|
2848
|
+
return missing.map(([k]) => k);
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
if (process.platform === 'darwin') {
|
|
2852
|
+
// launchd runs the gateway through an env-wrapper that sources this file, so patching it is the
|
|
2853
|
+
// launchd equivalent of adding Environment= lines to a unit.
|
|
2854
|
+
const envFile = join(projectDir, '.openclaw', 'service-env', `${label}.env`);
|
|
2855
|
+
if (!existsSync(envFile)) return [];
|
|
2856
|
+
const body = await fsp.readFile(envFile, 'utf8');
|
|
2857
|
+
const have = new Set([...body.matchAll(/^\s*export\s+([A-Z_0-9]+)=/gm)].map((m) => m[1]));
|
|
2858
|
+
const missing = Object.entries(want).filter(([k, v]) => !have.has(k) && v !== '');
|
|
2859
|
+
if (!missing.length) return [];
|
|
2860
|
+
const added = missing.map(([k, v]) => `export ${k}='${String(v).replace(/'/g, "'\\''")}'`).join('\n');
|
|
2861
|
+
await fsp.copyFile(envFile, `${envFile}.bak`).catch(() => {});
|
|
2862
|
+
await fsp.writeFile(envFile, `${body.replace(/\n*$/, '')}\n${added}\n`, 'utf8');
|
|
2863
|
+
sendLog(`[native] service env completed: ${missing.map(([k]) => k).join(', ')}`);
|
|
2864
|
+
return missing.map(([k]) => k);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
return [];
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
/**
|
|
2871
|
+
* Reunite a native project with the files an unset OPENCLAW_HOME scattered into `~/.openclaw`.
|
|
2872
|
+
*
|
|
2873
|
+
* This MUST run before syncNativeServiceEnv takes effect: once OPENCLAW_HOME is finally correct, the
|
|
2874
|
+
* plugin looks for its Zalo session inside the project — and if the credentials are still sitting in
|
|
2875
|
+
* the home directory it finds nothing and demands a fresh QR login. Copy (never move) so a failed
|
|
2876
|
+
* run leaves the working original in place; skip anything the project already has.
|
|
2877
|
+
*/
|
|
2878
|
+
async function adoptStrayNativeHome(projectDir) {
|
|
2879
|
+
if (!isNativeProject(projectDir)) return [];
|
|
2880
|
+
const projectHome = join(projectDir, '.openclaw');
|
|
2881
|
+
const strayHome = join(os.homedir(), '.openclaw');
|
|
2882
|
+
if (resolve(strayHome) === resolve(projectHome) || !existsSync(strayHome)) return [];
|
|
2883
|
+
const moved = [];
|
|
2884
|
+
const entries = await fsp.readdir(strayHome, { withFileTypes: true }).catch(() => []);
|
|
2885
|
+
for (const entry of entries) {
|
|
2886
|
+
// `state` is deliberately excluded: the project has its own live database and merging two
|
|
2887
|
+
// sqlite files is not something a copy can do correctly.
|
|
2888
|
+
const isCreds = /^zalo-connect-credentials.*\.json$/.test(entry.name);
|
|
2889
|
+
if (!isCreds && entry.name !== 'media') continue;
|
|
2890
|
+
const from = join(strayHome, entry.name);
|
|
2891
|
+
const to = join(projectHome, entry.name);
|
|
2892
|
+
if (existsSync(to)) continue;
|
|
2893
|
+
await fsp.cp(from, to, { recursive: true }).catch(() => {});
|
|
2894
|
+
if (existsSync(to)) moved.push(entry.name);
|
|
2895
|
+
}
|
|
2896
|
+
if (moved.length) {
|
|
2897
|
+
sendLog(`[migrate] Native: adopted ${moved.join(', ')} from ${strayHome} (written there while OPENCLAW_HOME was unset).`);
|
|
2898
|
+
}
|
|
2899
|
+
return moved;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2802
2902
|
/**
|
|
2803
2903
|
* `openclaw daemon install` has no `--system` flag, so on Linux the gateway becomes a systemd USER
|
|
2804
2904
|
* unit — and a user manager without linger is torn down when that user's last session exits. On a
|
|
@@ -2836,6 +2936,16 @@ async function ensureSystemdLinger() {
|
|
|
2836
2936
|
*/
|
|
2837
2937
|
async function ensureNativePlugins(projectDir, { restart = false } = {}) {
|
|
2838
2938
|
if (!isNativeProject(projectDir)) return [];
|
|
2939
|
+
// Same cleanup the container entrypoint does (docker-gen.js): an interrupted `plugins install`
|
|
2940
|
+
// leaves extensions/.openclaw-install-stage-XXXXXX behind, and it still carries a plugin manifest —
|
|
2941
|
+
// so the gateway logs "duplicate plugin id detected" every boot and a stale build competes with the
|
|
2942
|
+
// real one for the same id. Native has no entrypoint, so it has to happen here.
|
|
2943
|
+
const extRoot = join(projectDir, '.openclaw', 'extensions');
|
|
2944
|
+
for (const entry of await fsp.readdir(extRoot, { withFileTypes: true }).catch(() => [])) {
|
|
2945
|
+
if (!entry.isDirectory() || !entry.name.startsWith('.openclaw-install-stage-')) continue;
|
|
2946
|
+
await fsp.rm(join(extRoot, entry.name), { recursive: true, force: true }).catch(() => {});
|
|
2947
|
+
sendLog(`[native] removed abandoned plugin staging dir ${entry.name}`);
|
|
2948
|
+
}
|
|
2839
2949
|
let cfg = {};
|
|
2840
2950
|
try { cfg = JSON.parse(await fsp.readFile(join(projectDir, '.openclaw', 'openclaw.json'), 'utf8')); } catch {}
|
|
2841
2951
|
// learning-memory backs plugins.slots.contextEngine for every bot; zalo-connect only when a bot
|
|
@@ -2972,6 +3082,11 @@ async function startNativeRuntime({ projectDir, osChoice = '', gatewayPort, rout
|
|
|
2972
3082
|
const env = nativeEnv(projectDir);
|
|
2973
3083
|
await ensureSystemdLinger();
|
|
2974
3084
|
await run('openclaw', ['daemon', 'install', '--force', '--port', String(gwPort)], { cwd: projectDir, env });
|
|
3085
|
+
// Order is load-bearing: adopt the stray files FIRST, then complete the service env. The other way
|
|
3086
|
+
// round, the gateway boots with a corrected OPENCLAW_HOME, finds no Zalo session there, and asks
|
|
3087
|
+
// for a new QR scan even though a perfectly good session exists in the home directory.
|
|
3088
|
+
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}`));
|
|
2975
3090
|
await run('openclaw', ['daemon', 'start'], { cwd: projectDir, env });
|
|
2976
3091
|
// Let the first boot finish its state migrations here, while nothing else is competing for the
|
|
2977
3092
|
// lease. Every later action (create bot, install plugin) then restarts a settled gateway.
|
|
@@ -6658,4 +6773,4 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
|
|
|
6658
6773
|
]).catch(() => {});
|
|
6659
6774
|
}
|
|
6660
6775
|
|
|
6661
|
-
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 };
|
|
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 };
|
|
@@ -268,7 +268,13 @@ If setup reported a plugin install error, run this after the bot is running:
|
|
|
268
268
|
{
|
|
269
269
|
id: 'smart-route',
|
|
270
270
|
name: 'Smart Proxy (Auto Route)',
|
|
271
|
-
|
|
271
|
+
// smart-route fans out to whatever free upstreams the operator's combo holds, and the
|
|
272
|
+
// SMALLEST window in that pool is the real ceiling — many free models stop at 128k.
|
|
273
|
+
// Declaring 200k let sessions grow past what the route could actually accept: once
|
|
274
|
+
// full, even the compaction summarize call overflowed and every turn died with
|
|
275
|
+
// "auto-compaction could not recover" until /new. 131072 keeps compaction triggering
|
|
276
|
+
// (window - reserveTokens) while the summarize request still fits everywhere.
|
|
277
|
+
contextWindow: 131072,
|
|
272
278
|
maxTokens: 8192,
|
|
273
279
|
input: ['text', 'image'],
|
|
274
280
|
},
|
|
@@ -272,8 +272,12 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
272
272
|
// • imageMaxDimensionPx / imageQuality / contextLimits.toolResultMaxChars → keep one
|
|
273
273
|
// heavy turn (deep research, 4K chart read-back) from overflowing the context
|
|
274
274
|
// window mid tool-loop, which cannot be compacted and poisons the session.
|
|
275
|
+
// • smart-route contextWindow 200000 → 131072: the old declared window exceeded the
|
|
276
|
+
// smallest upstream in typical free-model combos, so full sessions deadlocked —
|
|
277
|
+
// the compaction summarize call itself overflowed and only /new recovered. Only the
|
|
278
|
+
// exact setup-written 200000 is rewritten; an operator's custom value is left alone.
|
|
275
279
|
// Each key is only filled in when absent, so an operator's own tuning is never clobbered.
|
|
276
|
-
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;}}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;}d.contextLimits=d.contextLimits||{};if(d.contextLimits.toolResultMaxChars===undefined){d.contextLimits.toolResultMaxChars=12000;ch=true;}}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
|
|
280
|
+
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;}}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;}d.contextLimits=d.contextLimits||{};if(d.contextLimits.toolResultMaxChars===undefined){d.contextLimits.toolResultMaxChars=12000;ch=true;}}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;ch=true;}}}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
|
|
277
281
|
// Companion backfill for the same older projects: their TOOLS.md was generated before the
|
|
278
282
|
// skill-authoring / long-turn guidance existed, and workspace files are only written when a
|
|
279
283
|
// bot is created — so a rebuild alone leaves the assistant stopping at "proposal awaiting
|
|
@@ -288,6 +292,17 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
288
292
|
'export OPENCLAW_HOME="${OPENCLAW_HOME:-$PWD/.openclaw}"',
|
|
289
293
|
'export OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"',
|
|
290
294
|
'mkdir -p "$OPENCLAW_HOME" "$OPENCLAW_STATE_DIR"',
|
|
295
|
+
// `openclaw plugins install` unpacks into extensions/.openclaw-install-stage-XXXXXX and removes
|
|
296
|
+
// it when it finishes. An interrupted install leaves the staging copy behind — and it still
|
|
297
|
+
// carries a plugin manifest, so the gateway logs "duplicate plugin id detected" on every boot
|
|
298
|
+
// and a stale build competes with the real one for the same id. Found on a production host: a
|
|
299
|
+
// zalo-connect 3.0.7 stage dir shadowing 3.0.17 for a week. Nothing is installing at entrypoint
|
|
300
|
+
// time, so any stage dir here is by definition abandoned.
|
|
301
|
+
'for stage in "$OPENCLAW_HOME"/extensions/.openclaw-install-stage-*; do',
|
|
302
|
+
' [ -d "$stage" ] || continue',
|
|
303
|
+
' echo "[entrypoint] removing abandoned plugin staging dir $(basename "$stage")"',
|
|
304
|
+
' rm -rf "$stage"',
|
|
305
|
+
'done',
|
|
291
306
|
'if [ "$OPENCLAW_STATE_DIR" != "$OPENCLAW_HOME" ]; then',
|
|
292
307
|
' for path in "$OPENCLAW_HOME"/*; do',
|
|
293
308
|
' [ -e "$path" ] || continue',
|
package/dist/web/app.js
CHANGED
|
@@ -413,7 +413,11 @@ function remoteAccessPanel(s = {}) {
|
|
|
413
413
|
const host = r.host || '<your-server-ip>';
|
|
414
414
|
const user = r.user || 'root';
|
|
415
415
|
const portOf = (raw, def) => { try { return new URL(raw).port || def; } catch { return def; } };
|
|
416
|
-
|
|
416
|
+
// zalo-mod's dashboard is gateway port + 1, not a fixed 18790 — the same rule the plugin card's
|
|
417
|
+
// "Open" button uses. Hardcoding it meant any project whose gateway is not on 18789 got a tunnel
|
|
418
|
+
// command missing the dashboard port, and the dashboard then simply failed to load with no clue why.
|
|
419
|
+
const gwPort = Number(portOf(s.gatewayUrl, 18789));
|
|
420
|
+
const ports = Array.from(new Set([r.uiPort || 51789, gwPort, Number(portOf(s.routerUrl, 20128)), gwPort + 1]));
|
|
417
421
|
const cmd = `ssh ${ports.map((p) => `-L ${p}:127.0.0.1:${p}`).join(' ')} ${user}@${host}`;
|
|
418
422
|
return `<details class="card" style="margin-top:12px;" ${r.headless ? 'open' : ''}>
|
|
419
423
|
<summary style="cursor:pointer; font-weight:600;">🌐 ${t('Mở từ máy khác (VPS/server)', 'Open from another machine (VPS/server)')}</summary>
|
|
@@ -1011,11 +1015,7 @@ function botListPanel(bots) {
|
|
|
1011
1015
|
const listItems = bots.map(b => {
|
|
1012
1016
|
const role = (b.role || b.desc || b.description || '').trim() || t('Tr\u1ee3 l\u00fd OpenClaw','OpenClaw assistant');
|
|
1013
1017
|
const isZalo = b.channel === 'zalo-personal';
|
|
1014
|
-
const
|
|
1015
|
-
const connection = !health ? t('Chưa rõ','Unknown') : health.running ? t('Đã kết nối','Connected') : health.lastError ? t('Mất kết nối','Disconnected') : t('Đang kết nối','Connecting');
|
|
1016
|
-
const login = !health ? t('Chưa rõ','Unknown') : health.sessionSaved ? t('Đã đăng nhập','Logged in') : t('Chưa đăng nhập','Not logged in');
|
|
1017
|
-
const connectionTone = health?.running ? 'ok' : health?.lastError ? 'bad' : 'warn';
|
|
1018
|
-
const loginTone = health?.sessionSaved ? 'ok' : health ? 'bad' : 'warn';
|
|
1018
|
+
const { connection, login, connectionTone, loginTone } = zaloHealthBadges(b);
|
|
1019
1019
|
return `<article class="bot-item ${state.activeBotId===b.id?'active':''}" data-bot-id="${escapeHtml(b.id)}"><div class="bot-item-actions"><button class="bot-edit" data-edit-bot="${escapeHtml(b.id)}" title="${t('Sửa bot','Edit bot')}" aria-label="${t('Sửa bot','Edit bot')}">${actionIcon('edit')}</button><button class="bot-delete" data-delete-bot="${escapeHtml(b.id)}" title="${t('X\u00f3a bot','Delete bot')}" aria-label="${t('X\u00f3a bot','Delete bot')}">×</button></div><div class="bot-item-title"><b>${escapeHtml(b.name)}</b></div><small title="${escapeHtml(role)}">${escapeHtml(role)}</small>${isZalo ? `<div class="zalo-bot-health"><div><span>${t('Kết nối','Connection')}</span><em class="${connectionTone}">${connection}</em></div><div><span>${t('Đăng nhập','Login')}</span><em class="${loginTone}">${login}</em></div></div>` : ''}</article>`;
|
|
1020
1020
|
});
|
|
1021
1021
|
listItems.push(`
|
|
@@ -1623,7 +1623,10 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1623
1623
|
body.projectDir = activeProjectDir();
|
|
1624
1624
|
body.channel = state.botChannel || 'telegram';
|
|
1625
1625
|
body.userTimezone = state.tz;
|
|
1626
|
-
|
|
1626
|
+
// Only the CREATE flow starts a Zalo QR login (the server kicks it off and sets
|
|
1627
|
+
// loginStarted). Editing a bot (rename, persona...) must never pop the QR modal —
|
|
1628
|
+
// the session is already saved and the PUT endpoint starts no login.
|
|
1629
|
+
if (body.channel === 'zalo-personal' && !state.botEditId) {
|
|
1627
1630
|
state.botModalOpen = false;
|
|
1628
1631
|
state.zaloLoginOpen = true;
|
|
1629
1632
|
state.zaloQrDataUrl = '';
|
|
@@ -1718,6 +1721,24 @@ function zaloAccountHealth(bot) {
|
|
|
1718
1721
|
|| accounts.find((account) => account.accountId === (bot?.accountId || 'default'))
|
|
1719
1722
|
|| null;
|
|
1720
1723
|
}
|
|
1724
|
+
// Connection/login labels for one bot. lastError wins over running: the gateway keeps
|
|
1725
|
+
// running=true while the Zalo listener is stuck in a retry loop (e.g. "Đăng nhập thất
|
|
1726
|
+
// bại"), so a running-first check painted dead bots green.
|
|
1727
|
+
function zaloHealthBadges(bot) {
|
|
1728
|
+
const health = bot?.channel === 'zalo-personal' ? zaloAccountHealth(bot) : null;
|
|
1729
|
+
const loginFailed = !!health?.lastError && /đăng nhập|log ?in|auth|credential|session/i.test(String(health.lastError));
|
|
1730
|
+
const connection = !health ? t('Chưa rõ','Unknown')
|
|
1731
|
+
: health.lastError ? t('Mất kết nối','Disconnected')
|
|
1732
|
+
: health.running ? t('Đã kết nối','Connected')
|
|
1733
|
+
: t('Đang kết nối','Connecting');
|
|
1734
|
+
const login = !health ? t('Chưa rõ','Unknown')
|
|
1735
|
+
: loginFailed ? t('Phiên hết hạn','Session expired')
|
|
1736
|
+
: health.sessionSaved ? t('Đã đăng nhập','Logged in')
|
|
1737
|
+
: t('Chưa đăng nhập','Not logged in');
|
|
1738
|
+
const connectionTone = !health ? 'warn' : health.lastError ? 'bad' : health.running ? 'ok' : 'warn';
|
|
1739
|
+
const loginTone = !health ? 'warn' : loginFailed ? 'bad' : health.sessionSaved ? 'ok' : 'bad';
|
|
1740
|
+
return { health, connection, login, connectionTone, loginTone };
|
|
1741
|
+
}
|
|
1721
1742
|
function zaloToolbar(channelBots = []) {
|
|
1722
1743
|
const active = channelBots.find((bot) => bot.id === state.activeBotId) || channelBots[0];
|
|
1723
1744
|
const health = zaloAccountHealth(active);
|
|
@@ -1729,6 +1750,42 @@ async function loadZaloHealth(silent=false){
|
|
|
1729
1750
|
try { state.zaloHealth = await api('/api/zalo/health' + projectQuery()); } catch (_) { state.zaloHealth = null; }
|
|
1730
1751
|
if (!silent) render();
|
|
1731
1752
|
}
|
|
1753
|
+
// Patch the connection/login badges of the rendered bot cards in place. render() rebuilds
|
|
1754
|
+
// the whole panel (killing focus/scroll), so the auto-refresh below must never call it.
|
|
1755
|
+
function updateZaloHealthDom() {
|
|
1756
|
+
const bots = state.install?.bots || [];
|
|
1757
|
+
document.querySelectorAll('.bot-item[data-bot-id] .zalo-bot-health').forEach((wrap) => {
|
|
1758
|
+
const id = wrap.closest('[data-bot-id]')?.dataset.botId;
|
|
1759
|
+
const bot = bots.find((b) => b.id === id);
|
|
1760
|
+
if (!bot) return;
|
|
1761
|
+
const badges = zaloHealthBadges(bot);
|
|
1762
|
+
const ems = wrap.querySelectorAll('em');
|
|
1763
|
+
if (ems[0]) { ems[0].className = badges.connectionTone; ems[0].textContent = badges.connection; }
|
|
1764
|
+
if (ems[1]) { ems[1].className = badges.loginTone; ems[1].textContent = badges.login; }
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
// Live status: poll the health endpoint while Zalo bots are on screen (the server caches
|
|
1768
|
+
// the ~3s CLI probe with a short TTL, so this stays cheap) and patch the badges in place —
|
|
1769
|
+
// no more pressing "Làm mới" to see a bot drop or come back.
|
|
1770
|
+
const ZALO_HEALTH_POLL_MS = 10000;
|
|
1771
|
+
let zaloHealthPollBusy = false;
|
|
1772
|
+
// Debounced instant refresh for log-visible state changes (login saved, container
|
|
1773
|
+
// restarted...) so the badges flip within ~2s instead of waiting out a poll tick.
|
|
1774
|
+
let zaloHealthRefreshTimer = null;
|
|
1775
|
+
function scheduleZaloHealthRefresh() {
|
|
1776
|
+
if (zaloHealthRefreshTimer) return;
|
|
1777
|
+
zaloHealthRefreshTimer = setTimeout(async () => {
|
|
1778
|
+
zaloHealthRefreshTimer = null;
|
|
1779
|
+
try { await loadZaloHealth(true); updateZaloHealthDom(); } catch (_) {}
|
|
1780
|
+
}, 1500);
|
|
1781
|
+
}
|
|
1782
|
+
setInterval(async () => {
|
|
1783
|
+
if (document.hidden || zaloHealthPollBusy) return;
|
|
1784
|
+
if (!activeProjectDir()) return;
|
|
1785
|
+
if (!document.querySelector('.bot-item[data-bot-id] .zalo-bot-health')) return;
|
|
1786
|
+
zaloHealthPollBusy = true;
|
|
1787
|
+
try { await loadZaloHealth(true); updateZaloHealthDom(); } finally { zaloHealthPollBusy = false; }
|
|
1788
|
+
}, ZALO_HEALTH_POLL_MS);
|
|
1732
1789
|
async function loadFeatureFlags(silent=false){
|
|
1733
1790
|
const botId=currentBotId();
|
|
1734
1791
|
if (!activeProjectDir()) { state.featureFlags = {}; state.featureInstalled = {}; state.featureVersions = {}; if (!silent) render(); return; }
|
|
@@ -1784,6 +1841,7 @@ function appendLogLine(line) {
|
|
|
1784
1841
|
});
|
|
1785
1842
|
return;
|
|
1786
1843
|
}
|
|
1844
|
+
if (/\[zalo-connect\].*(login saved|listener (connected|start failed)|restarted|login failed|logged out)/i.test(line)) scheduleZaloHealthRefresh();
|
|
1787
1845
|
const html = `<p>${escapeHtml(line)}</p>`;
|
|
1788
1846
|
if (state.zaloLoginOpen || /\[zalo-connect\]|zalo|qr|login|scan/i.test(line)) {
|
|
1789
1847
|
state.zaloLoginLines.push(cleanTerminalLine(line));
|