create-openclaw-bot 5.12.0 → 5.13.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 +21 -2
- package/README.vi.md +21 -2
- package/dist/server/local-server.js +147 -52
- package/dist/setup/shared/docker-gen.js +4 -1
- package/dist/web/app.js +23 -31
- package/dist/web/styles.css +3 -0
- 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.13.0-0EA5E9?style=for-the-badge" alt="Version 5.13.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>
|
|
@@ -27,12 +27,31 @@
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
<div align="center">
|
|
31
|
+
<a href="https://www.youtube.com/watch?v=hPusYX-5Pmw">
|
|
32
|
+
<img src="https://img.youtube.com/vi/hPusYX-5Pmw/maxresdefault.jpg" alt="Watch the OpenClaw setup video" width="820" />
|
|
33
|
+
</a>
|
|
34
|
+
<br />
|
|
35
|
+
<strong>▶ Watch the OpenClaw + Zalo setup video on YouTube</strong>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🆕 What's New in v5.13.0
|
|
41
|
+
|
|
42
|
+
- 🟢 **Live Zalo status on every bot card**: each personal Zalo bot now shows its own connection and login state, with a compact green badge when it is ready.
|
|
43
|
+
- 👥 **Accurate multi-account visibility**: Setup reads the real Zalo Connect runtime per account, so projects with several Zalo bots no longer collapse into an ambiguous shared status.
|
|
44
|
+
- ✨ **A cleaner Zalo control area**: Refresh and Login Again sit together above the bot list, while shared Zalo Connect and Zalo Mod versions move into the main status column.
|
|
45
|
+
|
|
46
|
+
<details>
|
|
47
|
+
<summary><b>Previous: What's new in v5.13.0</b></summary>
|
|
31
48
|
|
|
32
49
|
- 💬 **OpenClaw Zalo Connect, ready in one click**: create a personal Zalo bot or press **Zalo Login** and Setup prepares the channel automatically, then opens the QR flow in the dashboard.
|
|
33
50
|
- ⚡ **Install once, reconnect faster**: an existing Zalo Connect installation is reused instead of being downloaded again on every login or restart.
|
|
34
51
|
- ✨ **Cleaner native Zalo experience**: group mentions, reactions and moderation now run through one maintained Zalo runtime; the retired integration and Sticker add-on are gone from Setup.
|
|
35
52
|
|
|
53
|
+
</details>
|
|
54
|
+
|
|
36
55
|
<details>
|
|
37
56
|
<summary><b>Previous: What's new in v5.11.1</b></summary>
|
|
38
57
|
|
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.13.0-0EA5E9?style=for-the-badge" alt="Version 5.13.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>
|
|
@@ -27,12 +27,31 @@
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
<div align="center">
|
|
31
|
+
<a href="https://www.youtube.com/watch?v=hPusYX-5Pmw">
|
|
32
|
+
<img src="https://img.youtube.com/vi/hPusYX-5Pmw/maxresdefault.jpg" alt="Xem video hướng dẫn cài OpenClaw" width="820" />
|
|
33
|
+
</a>
|
|
34
|
+
<br />
|
|
35
|
+
<strong>▶ Xem video hướng dẫn cài OpenClaw + Zalo trên YouTube</strong>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🆕 Có gì mới trong v5.13.0
|
|
41
|
+
|
|
42
|
+
- 🟢 **Trạng thái Zalo trực tiếp trên từng thẻ bot**: mỗi bot Zalo cá nhân hiển thị riêng trạng thái kết nối và đăng nhập, kèm badge xanh gọn khi đã sẵn sàng.
|
|
43
|
+
- 👥 **Nhận diện đa tài khoản chính xác**: Setup đọc runtime Zalo Connect theo từng tài khoản thật, nên project có nhiều bot Zalo không còn bị gom thành một trạng thái “chưa rõ”.
|
|
44
|
+
- ✨ **Khu điều khiển Zalo gọn hơn**: Làm mới và Đăng nhập lại nằm cùng hàng phía trên danh sách bot; phiên bản Zalo Connect và Zalo Mod được đưa sang cột trạng thái chung.
|
|
45
|
+
|
|
46
|
+
<details>
|
|
47
|
+
<summary><b>Trước đó: Có gì mới trong v5.13.0</b></summary>
|
|
31
48
|
|
|
32
49
|
- 💬 **OpenClaw Zalo Connect, sẵn sàng trong một chạm**: tạo bot Zalo cá nhân hoặc bấm **Đăng nhập Zalo**, Setup tự chuẩn bị channel rồi mở luồng quét QR ngay trên dashboard.
|
|
33
50
|
- ⚡ **Cài một lần, kết nối lại nhanh hơn**: nếu Zalo Connect đã có, Setup dùng lại bản hiện tại thay vì tải lại mỗi lần đăng nhập hay restart.
|
|
34
51
|
- ✨ **Trải nghiệm Zalo native gọn hơn**: mention nhóm, reaction và quản trị dùng chung một Zalo runtime được duy trì; luồng tích hợp cũ và tiện ích Sticker đã được gỡ khỏi Setup.
|
|
35
52
|
|
|
53
|
+
</details>
|
|
54
|
+
|
|
36
55
|
<details>
|
|
37
56
|
<summary><b>Trước đó: Có gì mới trong v5.11.1</b></summary>
|
|
38
57
|
|
|
@@ -1221,6 +1221,12 @@ function mapAgentChannels(agent, cfg) {
|
|
|
1221
1221
|
return enabled.length === 1 ? enabled : [mapAgentChannel(agent, cfg)];
|
|
1222
1222
|
}
|
|
1223
1223
|
|
|
1224
|
+
function bindingChannelId(channel = '') {
|
|
1225
|
+
if (channel === 'zalo-personal') return 'zalo-connect';
|
|
1226
|
+
if (channel === 'zalo-bot') return 'zalo';
|
|
1227
|
+
return channel;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1224
1230
|
async function listConfiguredBots(projectDir) {
|
|
1225
1231
|
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
1226
1232
|
if (!projectDir || !existsSync(cfgPath)) return [];
|
|
@@ -1234,11 +1240,14 @@ async function listConfiguredBots(projectDir) {
|
|
|
1234
1240
|
const env = await readProjectEnv(projectDir);
|
|
1235
1241
|
const hasOwnWorkspace = !!agent.workspace;
|
|
1236
1242
|
const identityName = usableIdentityName(identity.name);
|
|
1237
|
-
return mapAgentChannels(agent, cfg).map((channel) =>
|
|
1243
|
+
return mapAgentChannels(agent, cfg).map((channel) => {
|
|
1244
|
+
const binding = (cfg.bindings || []).find((b) => b.agentId === agent.id && b.match?.channel === bindingChannelId(channel));
|
|
1245
|
+
return ({
|
|
1238
1246
|
id: agent.id,
|
|
1239
1247
|
name: (hasOwnWorkspace ? identityName : agent.name) || agent.name || identityName || agent.id,
|
|
1240
1248
|
role: identity.role || meta.role || agent.role || agent.desc || agent.description || '',
|
|
1241
1249
|
channel,
|
|
1250
|
+
accountId: binding?.match?.accountId || 'default',
|
|
1242
1251
|
workspace: agent.workspace || `.openclaw/${workspaceRelForAgent(agent, cfg, projectDir)}`,
|
|
1243
1252
|
agentDir: agent.agentDir,
|
|
1244
1253
|
persona: meta.persona || '',
|
|
@@ -1250,7 +1259,8 @@ async function listConfiguredBots(projectDir) {
|
|
|
1250
1259
|
pageAccessToken: channel === 'fb-messenger' ? (env.FB_MESSENGER_PAGE_ACCESS_TOKEN || '') : '',
|
|
1251
1260
|
appSecret: channel === 'fb-messenger' ? (env.FB_MESSENGER_APP_SECRET || '') : '',
|
|
1252
1261
|
verifyToken: channel === 'fb-messenger' ? (env.FB_MESSENGER_VERIFY_TOKEN || '') : '',
|
|
1253
|
-
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1254
1264
|
}));
|
|
1255
1265
|
return rows.flat();
|
|
1256
1266
|
}
|
|
@@ -1726,7 +1736,7 @@ async function startZaloConnectLogin(projectDir, accountId = 'default') {
|
|
|
1726
1736
|
sendLog(`[zalo-connect] Plugin missing — installing pinned ${ZALO_CONNECT_PLUGIN_SPEC}...`);
|
|
1727
1737
|
const repo = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[0];
|
|
1728
1738
|
const ref = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[1] || ZALO_CONNECT_VERSION;
|
|
1729
|
-
const installCmd = `tmp=/tmp/openclaw-plugin-zalo-connect-${ref}; rm -rf "$tmp"; git clone --depth 1 --branch "${ref}" "${repo}" "$tmp" && cd /home/node/project && openclaw plugins install "$tmp" 2>&1`;
|
|
1739
|
+
const installCmd = `tmp=/tmp/openclaw-plugin-zalo-connect-${ref}; rm -rf "$tmp"; git clone --depth 1 --branch "${ref}" "${repo}" "$tmp" && rm -rf "$tmp/.git" && cd /home/node/project && openclaw plugins install "$tmp" 2>&1`;
|
|
1730
1740
|
const inst = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', installCmd], { cwd: projectDir, shell: false });
|
|
1731
1741
|
const instOut = `${inst.stdout}\n${inst.stderr}`;
|
|
1732
1742
|
for (const line of instOut.split(/\r?\n/).filter(Boolean)) sendLog(`[zalo-connect] ${line}`);
|
|
@@ -1823,72 +1833,146 @@ function cancelZaloLogin() {
|
|
|
1823
1833
|
return { ok: true, cancelled: false };
|
|
1824
1834
|
}
|
|
1825
1835
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1836
|
+
function buildZaloHealthSnapshot(cfg = {}, statusJson = null, credentialNames = null, options = {}) {
|
|
1837
|
+
const containerRunning = options.containerRunning !== false;
|
|
1838
|
+
const textStatus = String(options.textStatus || '');
|
|
1839
|
+
const agents = new Map((cfg.agents?.list || []).map((agent) => [String(agent.id), agent]));
|
|
1840
|
+
const bindings = (cfg.bindings || []).filter((binding) => binding.match?.channel === 'zalo-connect');
|
|
1841
|
+
const runtimeAccounts = Array.isArray(statusJson?.channelAccounts?.['zalo-connect'])
|
|
1842
|
+
? statusJson.channelAccounts['zalo-connect']
|
|
1843
|
+
: [];
|
|
1844
|
+
const expected = [];
|
|
1845
|
+
const seen = new Set();
|
|
1846
|
+
for (const binding of bindings) {
|
|
1847
|
+
const accountId = String(binding.match?.accountId || 'default');
|
|
1848
|
+
if (seen.has(accountId)) continue;
|
|
1849
|
+
seen.add(accountId);
|
|
1850
|
+
expected.push({ accountId, agentId: String(binding.agentId || '') });
|
|
1851
|
+
}
|
|
1852
|
+
for (const runtime of runtimeAccounts) {
|
|
1853
|
+
const accountId = String(runtime?.accountId || 'default');
|
|
1854
|
+
if (seen.has(accountId)) continue;
|
|
1855
|
+
seen.add(accountId);
|
|
1856
|
+
expected.push({ accountId, agentId: '' });
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const credentialSet = Array.isArray(credentialNames)
|
|
1860
|
+
? new Set(credentialNames.map((name) => String(name || '').toLowerCase()))
|
|
1861
|
+
: null;
|
|
1862
|
+
const fallbackLines = textStatus.split(/\r?\n/).filter((line) => /zalo[- ]connect/i.test(line));
|
|
1863
|
+
const accounts = expected.map(({ accountId, agentId }) => {
|
|
1864
|
+
const runtime = runtimeAccounts.find((item) => String(item?.accountId || 'default') === accountId) || null;
|
|
1865
|
+
const line = fallbackLines.find((item) => accountId === 'default'
|
|
1866
|
+
? /\bdefault\b/i.test(item) || fallbackLines.length === 1
|
|
1867
|
+
: item.toLowerCase().includes(accountId.toLowerCase())) || '';
|
|
1868
|
+
const fallbackRunning = /running|connected|ready|\bok\b/i.test(line);
|
|
1869
|
+
const fallbackFailed = /stopped|disconnected|error|failed/i.test(line);
|
|
1870
|
+
const configured = runtime ? runtime.configured === true : !!line && !fallbackFailed;
|
|
1871
|
+
const running = runtime ? runtime.running === true : fallbackRunning;
|
|
1872
|
+
const lastError = runtime?.lastError || (fallbackFailed ? line.trim() : null);
|
|
1873
|
+
const credentialFile = accountId === 'default'
|
|
1874
|
+
? 'zalo-connect-credentials.json'
|
|
1875
|
+
: `zalo-connect-credentials-${accountId}.json`;
|
|
1876
|
+
const fileSaved = credentialSet ? credentialSet.has(credentialFile.toLowerCase()) : false;
|
|
1877
|
+
const agent = agents.get(agentId);
|
|
1878
|
+
return {
|
|
1879
|
+
accountId,
|
|
1880
|
+
agentId,
|
|
1881
|
+
name: agent?.name || agentId || accountId,
|
|
1882
|
+
configured,
|
|
1883
|
+
running,
|
|
1884
|
+
lastError,
|
|
1885
|
+
sessionSaved: fileSaved || runtime?.configured === true,
|
|
1886
|
+
};
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
const total = accounts.length;
|
|
1890
|
+
const running = accounts.filter((account) => account.running).length;
|
|
1891
|
+
const configured = accounts.filter((account) => account.configured).length;
|
|
1892
|
+
const failed = accounts.filter((account) => account.lastError || (!account.running && account.configured)).length;
|
|
1893
|
+
let channelStatus = 'unknown';
|
|
1894
|
+
if (!containerRunning) channelStatus = 'container-stopped';
|
|
1895
|
+
else if (total > 0 && running === total) channelStatus = 'connected';
|
|
1896
|
+
else if (running > 0) channelStatus = 'partial';
|
|
1897
|
+
else if (total > 0 && (failed > 0 || configured > 0)) channelStatus = 'disconnected';
|
|
1898
|
+
else if (statusJson || fallbackLines.length) channelStatus = 'starting';
|
|
1899
|
+
|
|
1900
|
+
return {
|
|
1901
|
+
backend: cfg.channels?.['zalo-connect']?.enabled ? 'zalo-connect' : '',
|
|
1902
|
+
containerRunning,
|
|
1903
|
+
channelStatus,
|
|
1904
|
+
channelStatusLine: fallbackLines.join(' | '),
|
|
1905
|
+
summary: { total, running, configured, failed },
|
|
1906
|
+
accounts,
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// ── Zalo health snapshot for the dashboard ──────────────────────────────────────
|
|
1911
|
+
// Runtime JSON is authoritative and account-aware. Text parsing remains only as a
|
|
1912
|
+
// compatibility fallback for older OpenClaw builds.
|
|
1830
1913
|
async function getZaloHealth(projectDir) {
|
|
1831
|
-
const
|
|
1832
|
-
backend: '',
|
|
1914
|
+
const meta = {
|
|
1833
1915
|
supportedVersion: ZALO_CONNECT_VERSION,
|
|
1834
1916
|
installedVersion: null,
|
|
1835
|
-
containerRunning: false,
|
|
1836
|
-
channelStatus: 'unknown',
|
|
1837
|
-
channelStatusLine: '',
|
|
1838
|
-
sessionSaved: false,
|
|
1839
1917
|
zaloModInstalled: false,
|
|
1840
|
-
|
|
1918
|
+
zaloModVersion: null,
|
|
1841
1919
|
};
|
|
1842
|
-
if (!projectDir) return
|
|
1920
|
+
if (!projectDir) return { ...buildZaloHealthSnapshot({}, null, null, { containerRunning: false }), ...meta };
|
|
1843
1921
|
let cfg = null;
|
|
1844
1922
|
try { cfg = JSON.parse(await fsp.readFile(join(projectDir, '.openclaw', 'openclaw.json'), 'utf8')); } catch {}
|
|
1845
|
-
if (!cfg) return
|
|
1846
|
-
|
|
1847
|
-
if (!out.backend) return out;
|
|
1848
|
-
const binding = (cfg.bindings || []).find((b) => b.match?.channel === out.backend);
|
|
1849
|
-
if (binding?.match?.accountId) out.accountId = binding.match.accountId;
|
|
1850
|
-
out.zaloModInstalled = !!(cfg.plugins?.entries?.['zalo-mod'] || cfg.plugins?.entries?.['openclaw-zalo-mod'])
|
|
1923
|
+
if (!cfg) return { ...buildZaloHealthSnapshot({}, null, null, { containerRunning: false }), ...meta };
|
|
1924
|
+
meta.zaloModInstalled = !!(cfg.plugins?.entries?.['zalo-mod'] || cfg.plugins?.entries?.['openclaw-zalo-mod'])
|
|
1851
1925
|
|| existsSync(join(projectDir, '.openclaw', 'extensions', 'zalo-mod'));
|
|
1926
|
+
meta.zaloModVersion = await getInstalledPluginVersion(projectDir, ['zalo-mod', 'openclaw-zalo-mod']) || null;
|
|
1927
|
+
if (meta.zaloModVersion) meta.zaloModInstalled = true;
|
|
1852
1928
|
|
|
1853
|
-
// Installed zalo-connect version — extensions/ is bind-mounted on macOS/Linux; fall back
|
|
1854
|
-
// to reading inside the container (Windows keeps extensions on a named volume).
|
|
1855
1929
|
const botContainer = getBotContainerName(projectDir);
|
|
1856
|
-
if (
|
|
1930
|
+
if (cfg.channels?.['zalo-connect']?.enabled) {
|
|
1857
1931
|
const manifestHost = join(projectDir, '.openclaw', 'extensions', 'zalo-connect', 'openclaw.plugin.json');
|
|
1858
1932
|
try {
|
|
1859
|
-
|
|
1933
|
+
meta.installedVersion = JSON.parse(await fsp.readFile(manifestHost, 'utf8')).version || null;
|
|
1860
1934
|
} catch {
|
|
1861
1935
|
try {
|
|
1862
1936
|
const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'cat "${OPENCLAW_HOME:-/home/node/project/.openclaw}/extensions/zalo-connect/openclaw.plugin.json" 2>/dev/null'], { cwd: projectDir, shell: false, timeout: 8000 });
|
|
1863
|
-
|
|
1937
|
+
meta.installedVersion = JSON.parse(String(r.stdout || '{}')).version || null;
|
|
1864
1938
|
} catch {}
|
|
1865
1939
|
}
|
|
1866
|
-
// QR session/credentials saved? (zalo-connect stores its credential file under the state dir)
|
|
1867
|
-
try {
|
|
1868
|
-
const credRoot = join(projectDir, '.openclaw', 'credentials');
|
|
1869
|
-
const names = existsSync(credRoot) ? await fsp.readdir(credRoot) : [];
|
|
1870
|
-
out.sessionSaved = names.some((n) => n.toLowerCase().includes('zalo-connect'));
|
|
1871
|
-
} catch {}
|
|
1872
1940
|
}
|
|
1873
1941
|
|
|
1942
|
+
let containerRunning = false;
|
|
1874
1943
|
try {
|
|
1875
1944
|
const r = await runCapture('docker', ['inspect', '-f', '{{.State.Running}}', botContainer], { shell: false, timeout: 8000 });
|
|
1876
|
-
|
|
1945
|
+
containerRunning = String(r.stdout || '').trim() === 'true';
|
|
1877
1946
|
} catch {}
|
|
1878
|
-
|
|
1947
|
+
let statusJson = null;
|
|
1948
|
+
let textStatus = '';
|
|
1949
|
+
let credentialNames = null;
|
|
1950
|
+
if (containerRunning) {
|
|
1879
1951
|
try {
|
|
1880
|
-
const r = await runCapture('docker', ['exec', botContainer, '
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1952
|
+
const r = await runCapture('docker', ['exec', botContainer, 'openclaw', 'channels', 'status', '--json'], { cwd: projectDir, shell: false, timeout: 20000 });
|
|
1953
|
+
statusJson = parseJsonText(String(r.stdout || '').trim(), null);
|
|
1954
|
+
} catch {}
|
|
1955
|
+
if (!statusJson) {
|
|
1956
|
+
try {
|
|
1957
|
+
const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'openclaw channels status 2>&1 || true'], { cwd: projectDir, shell: false, timeout: 20000 });
|
|
1958
|
+
textStatus = String(r.stdout || '');
|
|
1959
|
+
} catch {}
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
const script = "const fs=require('fs'),path=require('path'),os=require('os');const d=path.join(os.homedir(),'.openclaw');let a=[];try{a=fs.readdirSync(d).filter(n=>/^zalo-connect-credentials(?:-[^.]+)?\\.json$/i.test(n))}catch{}process.stdout.write(JSON.stringify(a))";
|
|
1963
|
+
const r = await runCapture('docker', ['exec', botContainer, 'node', '-e', script], { cwd: projectDir, shell: false, timeout: 8000 });
|
|
1964
|
+
credentialNames = parseJsonText(String(r.stdout || '[]').trim(), []);
|
|
1965
|
+
} catch {}
|
|
1966
|
+
try {
|
|
1967
|
+
const versions = await getContainerExtensionVersions(projectDir);
|
|
1968
|
+
const zaloModVersion = versions['zalo-mod'] || versions['openclaw-zalo-mod'] || '';
|
|
1969
|
+
if (zaloModVersion) {
|
|
1970
|
+
meta.zaloModInstalled = true;
|
|
1971
|
+
meta.zaloModVersion = zaloModVersion;
|
|
1972
|
+
}
|
|
1887
1973
|
} catch {}
|
|
1888
|
-
} else {
|
|
1889
|
-
out.channelStatus = 'container-stopped';
|
|
1890
1974
|
}
|
|
1891
|
-
return
|
|
1975
|
+
return { ...buildZaloHealthSnapshot(cfg, statusJson, credentialNames, { containerRunning, textStatus }), ...meta };
|
|
1892
1976
|
}
|
|
1893
1977
|
|
|
1894
1978
|
function getBotServiceName(projectDir) {
|
|
@@ -2239,7 +2323,10 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
|
|
|
2239
2323
|
const agentMetas = [];
|
|
2240
2324
|
const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
|
|
2241
2325
|
const cfg = buildOpenclawJson(common);
|
|
2242
|
-
|
|
2326
|
+
// A core project has no channel account yet. Keep its environment shared/credential-free;
|
|
2327
|
+
// writing the literal <your_bot_token> placeholder makes OpenClaw auto-detect Telegram and
|
|
2328
|
+
// emit a misleading "plugin not enabled" warning before the user has created any bot.
|
|
2329
|
+
const env = buildEnvFileContent({ ...common, apiKey: '', botToken: '', isSharedEnv: true });
|
|
2243
2330
|
await fsp.writeFile(join(openclawHome, 'openclaw.json'), JSON.stringify(cfg, null, 2), 'utf8');
|
|
2244
2331
|
await fsp.writeFile(join(projectDir, '.env'), env, 'utf8');
|
|
2245
2332
|
await syncExecApprovals(projectDir, cfg);
|
|
@@ -2486,12 +2573,16 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
|
|
|
2486
2573
|
await fsp.mkdir(dockerDir, { recursive: true });
|
|
2487
2574
|
const envContent = existsSync(rootEnvPath)
|
|
2488
2575
|
? await fsp.readFile(rootEnvPath, 'utf8')
|
|
2489
|
-
: buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
|
|
2576
|
+
: buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '', isSharedEnv: true });
|
|
2490
2577
|
await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
|
|
2491
2578
|
sendLog(`Docker env ready: ${dockerEnvPath}`);
|
|
2492
2579
|
await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
|
|
2493
2580
|
await applyResolved9RouterApiKey(projectDir).catch(() => {});
|
|
2494
|
-
|
|
2581
|
+
// The full compose start already created the bot container. Recreating it here used to
|
|
2582
|
+
// interrupt OpenClaw's first-boot state migration and leave its five-minute lease behind,
|
|
2583
|
+
// making a brand-new project appear to crash-loop until the lease expired. The config is
|
|
2584
|
+
// bind-mounted, so the resolved 9Router key does not require an immediate second recreate.
|
|
2585
|
+
probeCacheClear();
|
|
2495
2586
|
}
|
|
2496
2587
|
state.installed = true;
|
|
2497
2588
|
sendLog('✅ Install completed');
|
|
@@ -3226,6 +3317,10 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3226
3317
|
}
|
|
3227
3318
|
|
|
3228
3319
|
if (kind === 'plugin') {
|
|
3320
|
+
const installSpec = pluginInstallSpec(id);
|
|
3321
|
+
const installArgs = ['plugins', 'install', installSpec, '--force'];
|
|
3322
|
+
if (installSpec.startsWith('clawhub:')) installArgs.push('--acknowledge-clawhub-risk');
|
|
3323
|
+
|
|
3229
3324
|
let composeDir = null;
|
|
3230
3325
|
if (existsSync(join(projectDir, 'docker-compose.yml'))) {
|
|
3231
3326
|
composeDir = projectDir;
|
|
@@ -3235,9 +3330,9 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3235
3330
|
|
|
3236
3331
|
if (composeDir) {
|
|
3237
3332
|
const botContainer = getBotContainerName(projectDir);
|
|
3238
|
-
sendLog(`[plugin] Installing/updating ${
|
|
3333
|
+
sendLog(`[plugin] Installing/updating ${installSpec} inside container ${botContainer}...`);
|
|
3239
3334
|
|
|
3240
|
-
const cmd = `cd /home/node/project && openclaw
|
|
3335
|
+
const cmd = `cd /home/node/project && openclaw ${installArgs.join(' ')}`;
|
|
3241
3336
|
const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
|
|
3242
3337
|
|
|
3243
3338
|
if (cmdOut) {
|
|
@@ -3329,10 +3424,10 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3329
3424
|
} else {
|
|
3330
3425
|
// Fix any legacy config issues first
|
|
3331
3426
|
await run('openclaw', ['doctor', '--fix'], { cwd: projectDir, env: openclawProjectEnv(projectDir) }).catch((err) => sendLog(`[plugin] doctor --fix skipped: ${err.message}`));
|
|
3332
|
-
sendLog(`[plugin] Installing ${
|
|
3427
|
+
sendLog(`[plugin] Installing ${installSpec}...`);
|
|
3333
3428
|
|
|
3334
3429
|
let installSuccess = true;
|
|
3335
|
-
await run('openclaw',
|
|
3430
|
+
await run('openclaw', installArgs, {
|
|
3336
3431
|
cwd: projectDir,
|
|
3337
3432
|
env: openclawProjectEnv(projectDir),
|
|
3338
3433
|
resolveOnPattern: /Installed plugin:/
|
|
@@ -4115,4 +4210,4 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
|
|
|
4115
4210
|
printRemoteAccessHint(port).catch(() => {});
|
|
4116
4211
|
}
|
|
4117
4212
|
|
|
4118
|
-
export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder };
|
|
4213
|
+
export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, buildZaloHealthSnapshot };
|
|
@@ -197,7 +197,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
197
197
|
function buildDockerArtifacts(options) {
|
|
198
198
|
const {
|
|
199
199
|
openClawNpmSpec,
|
|
200
|
-
openClawRuntimePackages,
|
|
200
|
+
openClawRuntimePackages = '',
|
|
201
201
|
is9Router,
|
|
202
202
|
isLocal,
|
|
203
203
|
isMultiBot,
|
|
@@ -287,6 +287,9 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
287
287
|
' tmp="/tmp/openclaw-plugin-$id-$ref"',
|
|
288
288
|
' rm -rf "$tmp"',
|
|
289
289
|
' if git clone --depth 1 --branch "$ref" "$repo" "$tmp" 2>/dev/null; then',
|
|
290
|
+
' # Docker Desktop bind mounts can reject chmod on packed .git objects (EACCES).',
|
|
291
|
+
' # OpenClaw only needs the plugin payload, never the clone metadata.',
|
|
292
|
+
' rm -rf "$tmp/.git"',
|
|
290
293
|
' openclaw plugins install "$tmp" 2>/dev/null || echo "[entrypoint] warning: failed to install cloned plugin $id"',
|
|
291
294
|
' else',
|
|
292
295
|
' echo "[entrypoint] warning: failed to clone plugin $spec"',
|
package/dist/web/app.js
CHANGED
|
@@ -799,14 +799,13 @@ function botView() {
|
|
|
799
799
|
<div class="bot-main-content" style="margin-top: 14px;">
|
|
800
800
|
<div class="card-head" style="margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center;">
|
|
801
801
|
<h3 style="margin: 0; font-size: 16px;">${t('Danh sách bot','Bot list')}</h3>
|
|
802
|
-
${(ch === 'zalo-personal' && channelBots.length > 0) ?
|
|
802
|
+
${(ch === 'zalo-personal' && channelBots.length > 0) ? zaloToolbar(channelBots) : ''}
|
|
803
803
|
</div>
|
|
804
804
|
<div class="channel-tabs" style="margin-bottom: 16px;">${BOT_CHANNELS.map(c => c.comingSoon
|
|
805
805
|
? `<button class="is-coming-soon" disabled title="${t('Sắp ra mắt','Coming soon')}"><img src="${c.icon}" onerror="this.style.display='none'"/>${c.title}<span>${t('Sắp','Soon')}</span></button>`
|
|
806
806
|
: `<button class="${ch===c.id?'active':''}" data-bot-channel="${c.id}"><img src="${c.icon}" onerror="this.style.display='none'"/>${c.title}<span>${bots.filter(b=>b.channel===c.id).length}</span></button>`
|
|
807
807
|
).join('')}</div>
|
|
808
808
|
${botListPanel(channelBots)}
|
|
809
|
-
${ch === 'zalo-personal' ? zaloHealthCard(channelBots) : ''}
|
|
810
809
|
</div>
|
|
811
810
|
</section>
|
|
812
811
|
<div class="bot-side-col">
|
|
@@ -826,6 +825,8 @@ function botView() {
|
|
|
826
825
|
<div class="dash-version-list" style="margin-top: 18px;">
|
|
827
826
|
<div><span>OpenClaw</span><b>${escapeHtml(openclawVer || '-')}</b></div>
|
|
828
827
|
<div><span>9Router</span><b>${escapeHtml(routerVer || '-')}</b></div>
|
|
828
|
+
${ch === 'zalo-personal' ? `<div><span>Zalo Connect</span><b>${escapeHtml(state.zaloHealth?.installedVersion || '-')}</b></div>
|
|
829
|
+
<div><span>Zalo Mod</span><b>${state.zaloHealth ? escapeHtml(state.zaloHealth.zaloModVersion || (state.zaloHealth.zaloModInstalled ? '-' : t('Chưa cài','Not installed'))) : '…'}</b></div>` : ''}
|
|
829
830
|
</div>
|
|
830
831
|
</section>
|
|
831
832
|
</div>
|
|
@@ -896,7 +897,14 @@ function botCreateModal() {
|
|
|
896
897
|
function botListPanel(bots) {
|
|
897
898
|
const listItems = bots.map(b => {
|
|
898
899
|
const role = (b.role || b.desc || b.description || '').trim() || t('Tr\u1ee3 l\u00fd OpenClaw','OpenClaw assistant');
|
|
899
|
-
|
|
900
|
+
const isZalo = b.channel === 'zalo-personal';
|
|
901
|
+
const health = isZalo ? zaloAccountHealth(b) : null;
|
|
902
|
+
const quickBadge = health?.running ? `<span class="zalo-quick-badge">${t('Đã kết nối','Connected')}</span>` : '';
|
|
903
|
+
const connection = !health ? t('Chưa rõ','Unknown') : health.running ? t('Hoạt động','Active') : health.lastError ? t('Mất kết nối','Disconnected') : t('Đang kết nối','Connecting');
|
|
904
|
+
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');
|
|
905
|
+
const connectionTone = health?.running ? 'ok' : health?.lastError ? 'bad' : 'warn';
|
|
906
|
+
const loginTone = health?.sessionSaved ? 'ok' : health ? 'bad' : 'warn';
|
|
907
|
+
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>${quickBadge}</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>`;
|
|
900
908
|
});
|
|
901
909
|
listItems.push(`
|
|
902
910
|
<article class="bot-item bot-create-card" data-bot-modal="open">
|
|
@@ -1511,33 +1519,17 @@ async function loadFiles(silent=false){
|
|
|
1511
1519
|
if (!silent) render();
|
|
1512
1520
|
}
|
|
1513
1521
|
async function loadCatalog(silent=false){ state.catalog = await api('/api/catalog'); if (!silent) render(); }
|
|
1514
|
-
function
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
}
|
|
1525
|
-
const ver = h?.installedVersion ? `${h.installedVersion}${h.supportedVersion && h.installedVersion !== h.supportedVersion ? ` → ${h.supportedVersion}` : ''}` : (h ? t('chưa cài','not installed') : '…');
|
|
1526
|
-
return `<div class="card zalo-health-card" style="margin-top:14px; padding:14px;">
|
|
1527
|
-
<div class="card-head" style="margin-bottom:10px; display:flex; justify-content:space-between; align-items:center;">
|
|
1528
|
-
<h3 style="margin:0; font-size:14px;">🩺 Zalo (ZaloConnect)</h3>${statusChip}
|
|
1529
|
-
</div>
|
|
1530
|
-
<div class="dash-version-list" style="margin-top:0;">
|
|
1531
|
-
<div><span>ZaloConnect</span><b>${escapeHtml(ver)}</b></div>
|
|
1532
|
-
<div><span>${t('Phiên QR','QR session')}</span><b>${h ? (h.sessionSaved ? t('Đã lưu','Saved') : t('Chưa đăng nhập','Not logged in')) : '…'}</b></div>
|
|
1533
|
-
<div><span>Zalo Mod</span><b>${h ? (h.zaloModInstalled ? t('Đã cài','Installed') : t('Chưa cài','Not installed')) : '…'}</b></div>
|
|
1534
|
-
${h?.channelStatusLine ? `<div><span>${t('Kênh','Channel')}</span><b style="font-weight:500; font-size:11px; opacity:.8;">${escapeHtml(h.channelStatusLine.slice(0, 60))}</b></div>` : ''}
|
|
1535
|
-
</div>
|
|
1536
|
-
<div style="display:flex; gap:8px; margin-top:12px;">
|
|
1537
|
-
<button class="secondary icon-btn2" data-zalo-health-refresh type="button" style="flex:1; justify-content:center; font-size:12px; height:32px; border-width:1px;">🔄 ${t('Làm mới','Refresh')}</button>
|
|
1538
|
-
<button class="secondary icon-btn2" data-zalo-login-trigger type="button" style="flex:1; justify-content:center; font-size:12px; height:32px; border-width:1px;">🔑 ${t('Kết nối lại','Reconnect')}</button>
|
|
1539
|
-
</div>
|
|
1540
|
-
</div>`;
|
|
1522
|
+
function zaloAccountHealth(bot) {
|
|
1523
|
+
const accounts = state.zaloHealth?.accounts || [];
|
|
1524
|
+
return accounts.find((account) => account.agentId === bot?.id)
|
|
1525
|
+
|| accounts.find((account) => account.accountId === (bot?.accountId || 'default'))
|
|
1526
|
+
|| null;
|
|
1527
|
+
}
|
|
1528
|
+
function zaloToolbar(channelBots = []) {
|
|
1529
|
+
const active = channelBots.find((bot) => bot.id === state.activeBotId) || channelBots[0];
|
|
1530
|
+
const health = zaloAccountHealth(active);
|
|
1531
|
+
const loginLabel = health?.sessionSaved ? t('Đăng nhập lại','Log in again') : t('Đăng nhập Zalo','Zalo Login');
|
|
1532
|
+
return `<div class="zalo-toolbar"><button class="secondary btn-inline" data-zalo-health-refresh type="button">${actionIcon('refresh')}<span>${t('Làm mới','Refresh')}</span></button><button class="secondary btn-inline" data-zalo-login-trigger type="button">🔑 <span>${loginLabel}</span></button></div>`;
|
|
1541
1533
|
}
|
|
1542
1534
|
async function loadZaloHealth(silent=false){
|
|
1543
1535
|
if (!activeProjectDir()) { state.zaloHealth = null; return; }
|
|
@@ -1553,7 +1545,7 @@ async function loadFeatureFlags(silent=false){
|
|
|
1553
1545
|
state.featureInstalled = data.installed || {};
|
|
1554
1546
|
state.featureVersions = data.versions || {};
|
|
1555
1547
|
state.zaloBackend = data.zaloBackend || '';
|
|
1556
|
-
if (state.zaloBackend === 'zalo-connect') loadZaloHealth(true).catch(() => {});
|
|
1548
|
+
if (state.zaloBackend === 'zalo-connect') await loadZaloHealth(true).catch(() => {});
|
|
1557
1549
|
} catch (_) {
|
|
1558
1550
|
state.featureFlags = {};
|
|
1559
1551
|
state.featureInstalled = {};
|
package/dist/web/styles.css
CHANGED
|
@@ -1790,6 +1790,9 @@ html[data-theme="light"] .bottom .nav.active {
|
|
|
1790
1790
|
text-align: right !important;
|
|
1791
1791
|
}
|
|
1792
1792
|
|
|
1793
|
+
/* Compact per-account Zalo health — shared versions stay in the right status column. */
|
|
1794
|
+
.zalo-toolbar{display:flex;gap:8px;align-items:center}.zalo-toolbar .btn-inline{min-height:36px;padding:6px 12px;font-size:13px}.zalo-toolbar svg{width:14px;height:14px}.bot-item-title{display:flex;align-items:center;gap:8px;min-width:0;padding-right:58px}.bot-item-title b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.zalo-quick-badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:999px;border:1px solid rgba(24,194,156,.35);background:rgba(24,194,156,.12);color:#25d6ad;font-size:10px;font-weight:800;white-space:nowrap}.zalo-bot-health{display:grid;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid rgba(255,255,255,.07)}.zalo-bot-health>div{display:flex;align-items:center;justify-content:space-between;gap:10px}.zalo-bot-health span{color:var(--muted);font-size:11px;font-weight:700}.zalo-bot-health em{font-size:11px;font-style:normal;font-weight:800;text-align:right}.zalo-bot-health em.ok{color:#25d6ad}.zalo-bot-health em.warn{color:#f7bf48}.zalo-bot-health em.bad{color:#ff6574}html[data-theme="light"] .zalo-bot-health{border-top-color:rgba(15,23,42,.08)}@media(max-width:640px){.zalo-toolbar{display:grid;grid-template-columns:1fr 1fr;width:100%}.zalo-toolbar .btn-inline{justify-content:center;padding:6px 8px}.bot-item-title{padding-right:54px}}
|
|
1795
|
+
|
|
1793
1796
|
/* Ensure bottom navigation bar is visible on tablet and mobile (under 1180px) */
|
|
1794
1797
|
@media (max-width: 1180px) {
|
|
1795
1798
|
.bottom {
|