create-openclaw-bot 5.12.0 → 5.13.3
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 +162 -54
- package/dist/setup/data/channels.js +5 -5
- package/dist/setup/shared/bot-config-gen.js +6 -7
- package/dist/setup/shared/common-gen.js +2 -2
- package/dist/setup/shared/docker-gen.js +9 -6
- package/dist/web/app.js +22 -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.3-0EA5E9?style=for-the-badge" alt="Version 5.13.3" /></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.12.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.3-0EA5E9?style=for-the-badge" alt="Version 5.13.3" /></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.12.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
|
|
|
@@ -759,6 +759,10 @@ async function detectRuntime(projectDir) {
|
|
|
759
759
|
const _runtimeSynced = new Set();
|
|
760
760
|
async function syncRuntimeState(projectDir, { full = false } = {}) {
|
|
761
761
|
if (!projectDir || !existsSync(join(projectDir, '.openclaw', 'openclaw.json'))) return;
|
|
762
|
+
// New OpenClaw builds may create this directory during probing even when no
|
|
763
|
+
// attestation exists. Remove only the empty directory so it does not clutter
|
|
764
|
+
// the project tree; never touch a real `.attested` file.
|
|
765
|
+
await removeEmptyWorkspaceAttestations(projectDir).catch(() => {});
|
|
762
766
|
const firstSync = full || !_runtimeSynced.has(projectDir);
|
|
763
767
|
if (firstSync) {
|
|
764
768
|
// Auto-migrate legacy /root/project paths → /home/node/project in openclaw.json
|
|
@@ -786,6 +790,15 @@ async function syncRuntimeState(projectDir, { full = false } = {}) {
|
|
|
786
790
|
_runtimeSynced.add(projectDir);
|
|
787
791
|
}
|
|
788
792
|
|
|
793
|
+
async function removeEmptyWorkspaceAttestations(projectDir) {
|
|
794
|
+
const attestDir = join(projectDir, '.openclaw', 'workspace-attestations');
|
|
795
|
+
if (!existsSync(attestDir)) return false;
|
|
796
|
+
const entries = await fsp.readdir(attestDir);
|
|
797
|
+
if (entries.length > 0) return false;
|
|
798
|
+
await fsp.rmdir(attestDir);
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
|
|
789
802
|
/**
|
|
790
803
|
* Migrate legacy /root/project/ paths to /home/node/project/ in openclaw.json.
|
|
791
804
|
* Old projects may have been created with /root/project/ which doesn't match the
|
|
@@ -1221,6 +1234,12 @@ function mapAgentChannels(agent, cfg) {
|
|
|
1221
1234
|
return enabled.length === 1 ? enabled : [mapAgentChannel(agent, cfg)];
|
|
1222
1235
|
}
|
|
1223
1236
|
|
|
1237
|
+
function bindingChannelId(channel = '') {
|
|
1238
|
+
if (channel === 'zalo-personal') return 'zalo-connect';
|
|
1239
|
+
if (channel === 'zalo-bot') return 'zalo';
|
|
1240
|
+
return channel;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1224
1243
|
async function listConfiguredBots(projectDir) {
|
|
1225
1244
|
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
1226
1245
|
if (!projectDir || !existsSync(cfgPath)) return [];
|
|
@@ -1234,11 +1253,14 @@ async function listConfiguredBots(projectDir) {
|
|
|
1234
1253
|
const env = await readProjectEnv(projectDir);
|
|
1235
1254
|
const hasOwnWorkspace = !!agent.workspace;
|
|
1236
1255
|
const identityName = usableIdentityName(identity.name);
|
|
1237
|
-
return mapAgentChannels(agent, cfg).map((channel) =>
|
|
1256
|
+
return mapAgentChannels(agent, cfg).map((channel) => {
|
|
1257
|
+
const binding = (cfg.bindings || []).find((b) => b.agentId === agent.id && b.match?.channel === bindingChannelId(channel));
|
|
1258
|
+
return ({
|
|
1238
1259
|
id: agent.id,
|
|
1239
1260
|
name: (hasOwnWorkspace ? identityName : agent.name) || agent.name || identityName || agent.id,
|
|
1240
1261
|
role: identity.role || meta.role || agent.role || agent.desc || agent.description || '',
|
|
1241
1262
|
channel,
|
|
1263
|
+
accountId: binding?.match?.accountId || 'default',
|
|
1242
1264
|
workspace: agent.workspace || `.openclaw/${workspaceRelForAgent(agent, cfg, projectDir)}`,
|
|
1243
1265
|
agentDir: agent.agentDir,
|
|
1244
1266
|
persona: meta.persona || '',
|
|
@@ -1250,7 +1272,8 @@ async function listConfiguredBots(projectDir) {
|
|
|
1250
1272
|
pageAccessToken: channel === 'fb-messenger' ? (env.FB_MESSENGER_PAGE_ACCESS_TOKEN || '') : '',
|
|
1251
1273
|
appSecret: channel === 'fb-messenger' ? (env.FB_MESSENGER_APP_SECRET || '') : '',
|
|
1252
1274
|
verifyToken: channel === 'fb-messenger' ? (env.FB_MESSENGER_VERIFY_TOKEN || '') : '',
|
|
1253
|
-
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1254
1277
|
}));
|
|
1255
1278
|
return rows.flat();
|
|
1256
1279
|
}
|
|
@@ -1726,7 +1749,7 @@ async function startZaloConnectLogin(projectDir, accountId = 'default') {
|
|
|
1726
1749
|
sendLog(`[zalo-connect] Plugin missing — installing pinned ${ZALO_CONNECT_PLUGIN_SPEC}...`);
|
|
1727
1750
|
const repo = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[0];
|
|
1728
1751
|
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`;
|
|
1752
|
+
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
1753
|
const inst = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', installCmd], { cwd: projectDir, shell: false });
|
|
1731
1754
|
const instOut = `${inst.stdout}\n${inst.stderr}`;
|
|
1732
1755
|
for (const line of instOut.split(/\r?\n/).filter(Boolean)) sendLog(`[zalo-connect] ${line}`);
|
|
@@ -1823,72 +1846,146 @@ function cancelZaloLogin() {
|
|
|
1823
1846
|
return { ok: true, cancelled: false };
|
|
1824
1847
|
}
|
|
1825
1848
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1849
|
+
function buildZaloHealthSnapshot(cfg = {}, statusJson = null, credentialNames = null, options = {}) {
|
|
1850
|
+
const containerRunning = options.containerRunning !== false;
|
|
1851
|
+
const textStatus = String(options.textStatus || '');
|
|
1852
|
+
const agents = new Map((cfg.agents?.list || []).map((agent) => [String(agent.id), agent]));
|
|
1853
|
+
const bindings = (cfg.bindings || []).filter((binding) => binding.match?.channel === 'zalo-connect');
|
|
1854
|
+
const runtimeAccounts = Array.isArray(statusJson?.channelAccounts?.['zalo-connect'])
|
|
1855
|
+
? statusJson.channelAccounts['zalo-connect']
|
|
1856
|
+
: [];
|
|
1857
|
+
const expected = [];
|
|
1858
|
+
const seen = new Set();
|
|
1859
|
+
for (const binding of bindings) {
|
|
1860
|
+
const accountId = String(binding.match?.accountId || 'default');
|
|
1861
|
+
if (seen.has(accountId)) continue;
|
|
1862
|
+
seen.add(accountId);
|
|
1863
|
+
expected.push({ accountId, agentId: String(binding.agentId || '') });
|
|
1864
|
+
}
|
|
1865
|
+
for (const runtime of runtimeAccounts) {
|
|
1866
|
+
const accountId = String(runtime?.accountId || 'default');
|
|
1867
|
+
if (seen.has(accountId)) continue;
|
|
1868
|
+
seen.add(accountId);
|
|
1869
|
+
expected.push({ accountId, agentId: '' });
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const credentialSet = Array.isArray(credentialNames)
|
|
1873
|
+
? new Set(credentialNames.map((name) => String(name || '').toLowerCase()))
|
|
1874
|
+
: null;
|
|
1875
|
+
const fallbackLines = textStatus.split(/\r?\n/).filter((line) => /zalo[- ]connect/i.test(line));
|
|
1876
|
+
const accounts = expected.map(({ accountId, agentId }) => {
|
|
1877
|
+
const runtime = runtimeAccounts.find((item) => String(item?.accountId || 'default') === accountId) || null;
|
|
1878
|
+
const line = fallbackLines.find((item) => accountId === 'default'
|
|
1879
|
+
? /\bdefault\b/i.test(item) || fallbackLines.length === 1
|
|
1880
|
+
: item.toLowerCase().includes(accountId.toLowerCase())) || '';
|
|
1881
|
+
const fallbackRunning = /running|connected|ready|\bok\b/i.test(line);
|
|
1882
|
+
const fallbackFailed = /stopped|disconnected|error|failed/i.test(line);
|
|
1883
|
+
const configured = runtime ? runtime.configured === true : !!line && !fallbackFailed;
|
|
1884
|
+
const running = runtime ? runtime.running === true : fallbackRunning;
|
|
1885
|
+
const lastError = runtime?.lastError || (fallbackFailed ? line.trim() : null);
|
|
1886
|
+
const credentialFile = accountId === 'default'
|
|
1887
|
+
? 'zalo-connect-credentials.json'
|
|
1888
|
+
: `zalo-connect-credentials-${accountId}.json`;
|
|
1889
|
+
const fileSaved = credentialSet ? credentialSet.has(credentialFile.toLowerCase()) : false;
|
|
1890
|
+
const agent = agents.get(agentId);
|
|
1891
|
+
return {
|
|
1892
|
+
accountId,
|
|
1893
|
+
agentId,
|
|
1894
|
+
name: agent?.name || agentId || accountId,
|
|
1895
|
+
configured,
|
|
1896
|
+
running,
|
|
1897
|
+
lastError,
|
|
1898
|
+
sessionSaved: fileSaved || runtime?.configured === true,
|
|
1899
|
+
};
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
const total = accounts.length;
|
|
1903
|
+
const running = accounts.filter((account) => account.running).length;
|
|
1904
|
+
const configured = accounts.filter((account) => account.configured).length;
|
|
1905
|
+
const failed = accounts.filter((account) => account.lastError || (!account.running && account.configured)).length;
|
|
1906
|
+
let channelStatus = 'unknown';
|
|
1907
|
+
if (!containerRunning) channelStatus = 'container-stopped';
|
|
1908
|
+
else if (total > 0 && running === total) channelStatus = 'connected';
|
|
1909
|
+
else if (running > 0) channelStatus = 'partial';
|
|
1910
|
+
else if (total > 0 && (failed > 0 || configured > 0)) channelStatus = 'disconnected';
|
|
1911
|
+
else if (statusJson || fallbackLines.length) channelStatus = 'starting';
|
|
1912
|
+
|
|
1913
|
+
return {
|
|
1914
|
+
backend: cfg.channels?.['zalo-connect']?.enabled ? 'zalo-connect' : '',
|
|
1915
|
+
containerRunning,
|
|
1916
|
+
channelStatus,
|
|
1917
|
+
channelStatusLine: fallbackLines.join(' | '),
|
|
1918
|
+
summary: { total, running, configured, failed },
|
|
1919
|
+
accounts,
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// ── Zalo health snapshot for the dashboard ──────────────────────────────────────
|
|
1924
|
+
// Runtime JSON is authoritative and account-aware. Text parsing remains only as a
|
|
1925
|
+
// compatibility fallback for older OpenClaw builds.
|
|
1830
1926
|
async function getZaloHealth(projectDir) {
|
|
1831
|
-
const
|
|
1832
|
-
backend: '',
|
|
1927
|
+
const meta = {
|
|
1833
1928
|
supportedVersion: ZALO_CONNECT_VERSION,
|
|
1834
1929
|
installedVersion: null,
|
|
1835
|
-
containerRunning: false,
|
|
1836
|
-
channelStatus: 'unknown',
|
|
1837
|
-
channelStatusLine: '',
|
|
1838
|
-
sessionSaved: false,
|
|
1839
1930
|
zaloModInstalled: false,
|
|
1840
|
-
|
|
1931
|
+
zaloModVersion: null,
|
|
1841
1932
|
};
|
|
1842
|
-
if (!projectDir) return
|
|
1933
|
+
if (!projectDir) return { ...buildZaloHealthSnapshot({}, null, null, { containerRunning: false }), ...meta };
|
|
1843
1934
|
let cfg = null;
|
|
1844
1935
|
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'])
|
|
1936
|
+
if (!cfg) return { ...buildZaloHealthSnapshot({}, null, null, { containerRunning: false }), ...meta };
|
|
1937
|
+
meta.zaloModInstalled = !!(cfg.plugins?.entries?.['zalo-mod'] || cfg.plugins?.entries?.['openclaw-zalo-mod'])
|
|
1851
1938
|
|| existsSync(join(projectDir, '.openclaw', 'extensions', 'zalo-mod'));
|
|
1939
|
+
meta.zaloModVersion = await getInstalledPluginVersion(projectDir, ['zalo-mod', 'openclaw-zalo-mod']) || null;
|
|
1940
|
+
if (meta.zaloModVersion) meta.zaloModInstalled = true;
|
|
1852
1941
|
|
|
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
1942
|
const botContainer = getBotContainerName(projectDir);
|
|
1856
|
-
if (
|
|
1943
|
+
if (cfg.channels?.['zalo-connect']?.enabled) {
|
|
1857
1944
|
const manifestHost = join(projectDir, '.openclaw', 'extensions', 'zalo-connect', 'openclaw.plugin.json');
|
|
1858
1945
|
try {
|
|
1859
|
-
|
|
1946
|
+
meta.installedVersion = JSON.parse(await fsp.readFile(manifestHost, 'utf8')).version || null;
|
|
1860
1947
|
} catch {
|
|
1861
1948
|
try {
|
|
1862
1949
|
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
|
-
|
|
1950
|
+
meta.installedVersion = JSON.parse(String(r.stdout || '{}')).version || null;
|
|
1864
1951
|
} catch {}
|
|
1865
1952
|
}
|
|
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
1953
|
}
|
|
1873
1954
|
|
|
1955
|
+
let containerRunning = false;
|
|
1874
1956
|
try {
|
|
1875
1957
|
const r = await runCapture('docker', ['inspect', '-f', '{{.State.Running}}', botContainer], { shell: false, timeout: 8000 });
|
|
1876
|
-
|
|
1958
|
+
containerRunning = String(r.stdout || '').trim() === 'true';
|
|
1877
1959
|
} catch {}
|
|
1878
|
-
|
|
1960
|
+
let statusJson = null;
|
|
1961
|
+
let textStatus = '';
|
|
1962
|
+
let credentialNames = null;
|
|
1963
|
+
if (containerRunning) {
|
|
1879
1964
|
try {
|
|
1880
|
-
const r = await runCapture('docker', ['exec', botContainer, '
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1965
|
+
const r = await runCapture('docker', ['exec', botContainer, 'openclaw', 'channels', 'status', '--json'], { cwd: projectDir, shell: false, timeout: 20000 });
|
|
1966
|
+
statusJson = parseJsonText(String(r.stdout || '').trim(), null);
|
|
1967
|
+
} catch {}
|
|
1968
|
+
if (!statusJson) {
|
|
1969
|
+
try {
|
|
1970
|
+
const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'openclaw channels status 2>&1 || true'], { cwd: projectDir, shell: false, timeout: 20000 });
|
|
1971
|
+
textStatus = String(r.stdout || '');
|
|
1972
|
+
} catch {}
|
|
1973
|
+
}
|
|
1974
|
+
try {
|
|
1975
|
+
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))";
|
|
1976
|
+
const r = await runCapture('docker', ['exec', botContainer, 'node', '-e', script], { cwd: projectDir, shell: false, timeout: 8000 });
|
|
1977
|
+
credentialNames = parseJsonText(String(r.stdout || '[]').trim(), []);
|
|
1978
|
+
} catch {}
|
|
1979
|
+
try {
|
|
1980
|
+
const versions = await getContainerExtensionVersions(projectDir);
|
|
1981
|
+
const zaloModVersion = versions['zalo-mod'] || versions['openclaw-zalo-mod'] || '';
|
|
1982
|
+
if (zaloModVersion) {
|
|
1983
|
+
meta.zaloModInstalled = true;
|
|
1984
|
+
meta.zaloModVersion = zaloModVersion;
|
|
1985
|
+
}
|
|
1887
1986
|
} catch {}
|
|
1888
|
-
} else {
|
|
1889
|
-
out.channelStatus = 'container-stopped';
|
|
1890
1987
|
}
|
|
1891
|
-
return
|
|
1988
|
+
return { ...buildZaloHealthSnapshot(cfg, statusJson, credentialNames, { containerRunning, textStatus }), ...meta };
|
|
1892
1989
|
}
|
|
1893
1990
|
|
|
1894
1991
|
function getBotServiceName(projectDir) {
|
|
@@ -2239,7 +2336,10 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
|
|
|
2239
2336
|
const agentMetas = [];
|
|
2240
2337
|
const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
|
|
2241
2338
|
const cfg = buildOpenclawJson(common);
|
|
2242
|
-
|
|
2339
|
+
// A core project has no channel account yet. Keep its environment shared/credential-free;
|
|
2340
|
+
// writing the literal <your_bot_token> placeholder makes OpenClaw auto-detect Telegram and
|
|
2341
|
+
// emit a misleading "plugin not enabled" warning before the user has created any bot.
|
|
2342
|
+
const env = buildEnvFileContent({ ...common, apiKey: '', botToken: '', isSharedEnv: true });
|
|
2243
2343
|
await fsp.writeFile(join(openclawHome, 'openclaw.json'), JSON.stringify(cfg, null, 2), 'utf8');
|
|
2244
2344
|
await fsp.writeFile(join(projectDir, '.env'), env, 'utf8');
|
|
2245
2345
|
await syncExecApprovals(projectDir, cfg);
|
|
@@ -2486,12 +2586,16 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
|
|
|
2486
2586
|
await fsp.mkdir(dockerDir, { recursive: true });
|
|
2487
2587
|
const envContent = existsSync(rootEnvPath)
|
|
2488
2588
|
? 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: '' });
|
|
2589
|
+
: buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '', isSharedEnv: true });
|
|
2490
2590
|
await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
|
|
2491
2591
|
sendLog(`Docker env ready: ${dockerEnvPath}`);
|
|
2492
2592
|
await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
|
|
2493
2593
|
await applyResolved9RouterApiKey(projectDir).catch(() => {});
|
|
2494
|
-
|
|
2594
|
+
// The full compose start already created the bot container. Recreating it here used to
|
|
2595
|
+
// interrupt OpenClaw's first-boot state migration and leave its five-minute lease behind,
|
|
2596
|
+
// making a brand-new project appear to crash-loop until the lease expired. The config is
|
|
2597
|
+
// bind-mounted, so the resolved 9Router key does not require an immediate second recreate.
|
|
2598
|
+
probeCacheClear();
|
|
2495
2599
|
}
|
|
2496
2600
|
state.installed = true;
|
|
2497
2601
|
sendLog('✅ Install completed');
|
|
@@ -3185,7 +3289,7 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3185
3289
|
const botContainer = getBotContainerName(projectDir);
|
|
3186
3290
|
sendLog(`[skill] Installing/updating clawhub:${slug} inside container ${botContainer} for agent ${agentId}...`);
|
|
3187
3291
|
|
|
3188
|
-
const cmd = `cd /home/node/project && openclaw skills install ${slug} --agent ${agentId} --force`;
|
|
3292
|
+
const cmd = `cd /home/node/project && openclaw skills install ${slug} --agent ${agentId} --force --acknowledge-clawhub-risk`;
|
|
3189
3293
|
const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
|
|
3190
3294
|
|
|
3191
3295
|
if (cmdOut) {
|
|
@@ -3207,7 +3311,7 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3207
3311
|
await run('openclaw', ['doctor', '--fix'], { cwd: projectDir, env: openclawProjectEnv(projectDir) }).catch((err) => sendLog(`[skill] doctor --fix skipped: ${err.message}`));
|
|
3208
3312
|
sendLog(`[skill] Installing clawhub:${slug} for agent ${agentId}...`);
|
|
3209
3313
|
|
|
3210
|
-
await run('openclaw', ['skills', 'install', slug, '--agent', agentId, '--force'], {
|
|
3314
|
+
await run('openclaw', ['skills', 'install', slug, '--agent', agentId, '--force', '--acknowledge-clawhub-risk'], {
|
|
3211
3315
|
cwd: projectDir,
|
|
3212
3316
|
env: openclawProjectEnv(projectDir)
|
|
3213
3317
|
});
|
|
@@ -3226,6 +3330,10 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3226
3330
|
}
|
|
3227
3331
|
|
|
3228
3332
|
if (kind === 'plugin') {
|
|
3333
|
+
const installSpec = pluginInstallSpec(id);
|
|
3334
|
+
const installArgs = ['plugins', 'install', installSpec, '--force'];
|
|
3335
|
+
if (installSpec.startsWith('clawhub:')) installArgs.push('--acknowledge-clawhub-risk');
|
|
3336
|
+
|
|
3229
3337
|
let composeDir = null;
|
|
3230
3338
|
if (existsSync(join(projectDir, 'docker-compose.yml'))) {
|
|
3231
3339
|
composeDir = projectDir;
|
|
@@ -3235,9 +3343,9 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3235
3343
|
|
|
3236
3344
|
if (composeDir) {
|
|
3237
3345
|
const botContainer = getBotContainerName(projectDir);
|
|
3238
|
-
sendLog(`[plugin] Installing/updating ${
|
|
3346
|
+
sendLog(`[plugin] Installing/updating ${installSpec} inside container ${botContainer}...`);
|
|
3239
3347
|
|
|
3240
|
-
const cmd = `cd /home/node/project && openclaw
|
|
3348
|
+
const cmd = `cd /home/node/project && openclaw ${installArgs.join(' ')}`;
|
|
3241
3349
|
const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
|
|
3242
3350
|
|
|
3243
3351
|
if (cmdOut) {
|
|
@@ -3329,10 +3437,10 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3329
3437
|
} else {
|
|
3330
3438
|
// Fix any legacy config issues first
|
|
3331
3439
|
await run('openclaw', ['doctor', '--fix'], { cwd: projectDir, env: openclawProjectEnv(projectDir) }).catch((err) => sendLog(`[plugin] doctor --fix skipped: ${err.message}`));
|
|
3332
|
-
sendLog(`[plugin] Installing ${
|
|
3440
|
+
sendLog(`[plugin] Installing ${installSpec}...`);
|
|
3333
3441
|
|
|
3334
3442
|
let installSuccess = true;
|
|
3335
|
-
await run('openclaw',
|
|
3443
|
+
await run('openclaw', installArgs, {
|
|
3336
3444
|
cwd: projectDir,
|
|
3337
3445
|
env: openclawProjectEnv(projectDir),
|
|
3338
3446
|
resolveOnPattern: /Installed plugin:/
|
|
@@ -4115,4 +4223,4 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
|
|
|
4115
4223
|
printRemoteAccessHint(port).catch(() => {});
|
|
4116
4224
|
}
|
|
4117
4225
|
|
|
4118
|
-
export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder };
|
|
4226
|
+
export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, buildZaloHealthSnapshot, removeEmptyWorkspaceAttestations };
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
credSteps: [
|
|
63
63
|
{ textVi: '⚠️ Zalo cá nhân chạy qua <strong>OpenClaw Zalo Connect (unofficial, zca-js)</strong> — tự động hoá tài khoản cá nhân có thể vi phạm điều khoản Zalo và khiến tài khoản bị hạn chế. <strong>Chỉ nên dùng tài khoản phụ.</strong>', textEn: '⚠️ Personal Zalo runs on <strong>OpenClaw Zalo Connect (unofficial, zca-js)</strong> — automating a personal account may violate Zalo terms and can get the account restricted. <strong>Use a secondary account.</strong>' },
|
|
64
64
|
{ textVi: 'Sau khi tạo bot, bấm <strong>Đăng nhập Zalo</strong> để quét QR ngay trong giao diện. Nếu cần chạy thủ công: <code>openclaw channels login --channel zalo-connect --account default</code>.', textEn: 'After creating the bot, click <strong>Zalo Login</strong> to scan the QR right in the UI. Manual fallback: <code>openclaw channels login --channel zalo-connect --account default</code>.' },
|
|
65
|
-
{ textVi: '
|
|
65
|
+
{ textVi: 'Sau khi quét QR, bot nhận được tin nhắn xác nhận Owner ngay; bạn có thể siết lại DM/nhóm sau trong cấu hình.', textEn: 'After QR login, the bot can receive the initial Owner confirmation immediately; you can tighten DM/group access later in config.' },
|
|
66
66
|
],
|
|
67
67
|
// Mirrors buildZaloConnectChannelConfig() in bot-config-gen.js — keys validated
|
|
68
|
-
// against OpenClaw Zalo Connect 3.0.
|
|
68
|
+
// against OpenClaw Zalo Connect 3.0.1's strict channel schema.
|
|
69
69
|
channelConfig: {
|
|
70
70
|
'zalo-connect': {
|
|
71
71
|
enabled: true,
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
accounts: {
|
|
74
74
|
default: { enabled: true },
|
|
75
75
|
},
|
|
76
|
-
dmPolicy: '
|
|
77
|
-
allowFrom: [],
|
|
76
|
+
dmPolicy: 'open',
|
|
77
|
+
allowFrom: ['*'],
|
|
78
78
|
groupPolicy: 'allowlist',
|
|
79
79
|
groups: {
|
|
80
|
-
'*': { enabled:
|
|
80
|
+
'*': { enabled: true, requireMention: false },
|
|
81
81
|
},
|
|
82
82
|
},
|
|
83
83
|
},
|
|
@@ -310,10 +310,9 @@
|
|
|
310
310
|
channels.telegram = telegramConfig;
|
|
311
311
|
} else if (isZaloPersonal(channelKey)) {
|
|
312
312
|
// Zalo Personal → OpenClaw Zalo Connect, the only personal-Zalo backend.
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// Every key below is validated against OpenClaw Zalo Connect 3.0.0's schema
|
|
313
|
+
// First-run defaults keep owner confirmation reachable immediately after QR:
|
|
314
|
+
// DMs are open to every sender and groups are enabled without requiring a mention.
|
|
315
|
+
// Every key below is validated against OpenClaw Zalo Connect 3.0.1's schema
|
|
317
316
|
// (`additionalProperties: false`) — do NOT add keys (e.g. historyLimit,
|
|
318
317
|
// groupAllowFrom) without re-checking `openclaw.plugin.json` first, an unknown
|
|
319
318
|
// key crashes the gateway on boot.
|
|
@@ -336,11 +335,11 @@
|
|
|
336
335
|
accounts: {
|
|
337
336
|
default: { enabled: true },
|
|
338
337
|
},
|
|
339
|
-
dmPolicy: '
|
|
340
|
-
allowFrom: [],
|
|
338
|
+
dmPolicy: 'open',
|
|
339
|
+
allowFrom: ['*'],
|
|
341
340
|
groupPolicy: 'allowlist',
|
|
342
341
|
groups: {
|
|
343
|
-
'*': { enabled:
|
|
342
|
+
'*': { enabled: true, requireMention: false },
|
|
344
343
|
},
|
|
345
344
|
};
|
|
346
345
|
}
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
// Pin the maintained fork tag so setup never falls back to upstream/ClawHub.
|
|
45
45
|
const ZALO_CHANNEL_ID = 'zalo-connect';
|
|
46
46
|
const ZALO_PLUGIN_ID = 'zalo-connect';
|
|
47
|
-
const ZALO_CONNECT_VERSION = '3.0.
|
|
48
|
-
const ZALO_CONNECT_PLUGIN_SPEC = 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.
|
|
47
|
+
const ZALO_CONNECT_VERSION = '3.0.1';
|
|
48
|
+
const ZALO_CONNECT_PLUGIN_SPEC = 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.1';
|
|
49
49
|
|
|
50
50
|
function buildRelayPluginInstallCommand(prefix = 'openclaw') {
|
|
51
51
|
return `if [ ! -d "$OPENCLAW_STATE_DIR/extensions/${TELEGRAM_RELAY_PLUGIN_ID}" ]; then ${prefix} plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC} 2>/dev/null || true; fi`;
|
|
@@ -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,
|
|
@@ -244,10 +244,10 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
244
244
|
const stateVolDecl = useStateVolume ? '\n openclaw-state:' : '';
|
|
245
245
|
const stateVolBlock = useStateVolume ? '\n\nvolumes:\n openclaw-state:' : '';
|
|
246
246
|
const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
|
|
247
|
-
? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
|
|
247
|
+
? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} --acknowledge-clawhub-risk || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
|
|
248
248
|
: '';
|
|
249
249
|
const pluginLines = dockerfilePlugins.length > 0
|
|
250
|
-
? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
|
|
250
|
+
? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} --acknowledge-clawhub-risk || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
|
|
251
251
|
: '';
|
|
252
252
|
const patchLine = `RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"`;
|
|
253
253
|
|
|
@@ -287,12 +287,15 @@ 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"',
|
|
293
296
|
' fi',
|
|
294
297
|
' ;;',
|
|
295
|
-
' *) openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec" ;;',
|
|
298
|
+
' *) openclaw plugins install "$spec" --acknowledge-clawhub-risk 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec" ;;',
|
|
296
299
|
' esac',
|
|
297
300
|
'}',
|
|
298
301
|
'ensure_skill() {',
|
|
@@ -302,7 +305,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
302
305
|
' return 0',
|
|
303
306
|
' fi',
|
|
304
307
|
' echo "[entrypoint] skill $id missing; installing"',
|
|
305
|
-
' openclaw skills install "$id" 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
|
|
308
|
+
' openclaw skills install "$id" --acknowledge-clawhub-risk 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
|
|
306
309
|
'}',
|
|
307
310
|
'echo "[entrypoint] ensuring runtime assets, then starting gateway"',
|
|
308
311
|
];
|
|
@@ -317,7 +320,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
|
|
|
317
320
|
// install when extensions/zalo-connect already exists, so restarts never re-download
|
|
318
321
|
// and a reconnect never reinstalls. No dist patching, no mentions.js, no watchdog
|
|
319
322
|
// hacks: sticker/mention/reaction are native ZaloConnect actions.
|
|
320
|
-
const zaloConnectSpec = common.ZALO_CONNECT_PLUGIN_SPEC || 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.
|
|
323
|
+
const zaloConnectSpec = common.ZALO_CONNECT_PLUGIN_SPEC || 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.1';
|
|
321
324
|
runtimeParts.push(`ensure_plugin zalo-connect "${zaloConnectSpec}"`);
|
|
322
325
|
}
|
|
323
326
|
runtimeParts.push('openclaw gateway run');
|
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,13 @@ 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 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');
|
|
903
|
+
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');
|
|
904
|
+
const connectionTone = health?.running ? 'ok' : health?.lastError ? 'bad' : 'warn';
|
|
905
|
+
const loginTone = health?.sessionSaved ? 'ok' : health ? 'bad' : 'warn';
|
|
906
|
+
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>`;
|
|
900
907
|
});
|
|
901
908
|
listItems.push(`
|
|
902
909
|
<article class="bot-item bot-create-card" data-bot-modal="open">
|
|
@@ -1511,33 +1518,17 @@ async function loadFiles(silent=false){
|
|
|
1511
1518
|
if (!silent) render();
|
|
1512
1519
|
}
|
|
1513
1520
|
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>`;
|
|
1521
|
+
function zaloAccountHealth(bot) {
|
|
1522
|
+
const accounts = state.zaloHealth?.accounts || [];
|
|
1523
|
+
return accounts.find((account) => account.agentId === bot?.id)
|
|
1524
|
+
|| accounts.find((account) => account.accountId === (bot?.accountId || 'default'))
|
|
1525
|
+
|| null;
|
|
1526
|
+
}
|
|
1527
|
+
function zaloToolbar(channelBots = []) {
|
|
1528
|
+
const active = channelBots.find((bot) => bot.id === state.activeBotId) || channelBots[0];
|
|
1529
|
+
const health = zaloAccountHealth(active);
|
|
1530
|
+
const loginLabel = health?.sessionSaved ? t('Đăng nhập lại','Log in again') : t('Đăng nhập Zalo','Zalo Login');
|
|
1531
|
+
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
1532
|
}
|
|
1542
1533
|
async function loadZaloHealth(silent=false){
|
|
1543
1534
|
if (!activeProjectDir()) { state.zaloHealth = null; return; }
|
|
@@ -1553,7 +1544,7 @@ async function loadFeatureFlags(silent=false){
|
|
|
1553
1544
|
state.featureInstalled = data.installed || {};
|
|
1554
1545
|
state.featureVersions = data.versions || {};
|
|
1555
1546
|
state.zaloBackend = data.zaloBackend || '';
|
|
1556
|
-
if (state.zaloBackend === 'zalo-connect') loadZaloHealth(true).catch(() => {});
|
|
1547
|
+
if (state.zaloBackend === 'zalo-connect') await loadZaloHealth(true).catch(() => {});
|
|
1557
1548
|
} catch (_) {
|
|
1558
1549
|
state.featureFlags = {};
|
|
1559
1550
|
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 {
|