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 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.12.0-0EA5E9?style=for-the-badge" alt="Version 5.12.0" /></a>
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
- ## 🆕 What's New in v5.12.0
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.12.0-0EA5E9?style=for-the-badge" alt="Version 5.12.0" /></a>
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
- ## 🆕 Có gì mới trong v5.12.0
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
- // ── Zalo health snapshot for the dashboard card ─────────────────────────────────
1827
- // One cheap, non-throwing aggregate: backend, pinned vs installed version, channel
1828
- // status (parsed from `openclaw channels status`), QR/session presence, Zalo Mod
1829
- // presence. Everything degrades to null/'unknown' instead of failing the request.
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 out = {
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
- accountId: 'default',
1931
+ zaloModVersion: null,
1841
1932
  };
1842
- if (!projectDir) return out;
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 out;
1846
- if (cfg.channels?.['zalo-connect']?.enabled) out.backend = 'zalo-connect';
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 (out.backend === 'zalo-connect') {
1943
+ if (cfg.channels?.['zalo-connect']?.enabled) {
1857
1944
  const manifestHost = join(projectDir, '.openclaw', 'extensions', 'zalo-connect', 'openclaw.plugin.json');
1858
1945
  try {
1859
- out.installedVersion = JSON.parse(await fsp.readFile(manifestHost, 'utf8')).version || null;
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
- out.installedVersion = JSON.parse(String(r.stdout || '{}')).version || null;
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
- out.containerRunning = String(r.stdout || '').trim() === 'true';
1958
+ containerRunning = String(r.stdout || '').trim() === 'true';
1877
1959
  } catch {}
1878
- if (out.containerRunning) {
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, 'sh', '-lc', 'openclaw channels status 2>&1 || true'], { cwd: projectDir, shell: false, timeout: 20000 });
1881
- const needle = out.backend === 'zalo-connect' ? 'zalo-connect' : 'zalo';
1882
- const line = String(r.stdout || '').split(/\r?\n/).find((l) => l.toLowerCase().includes(needle)) || '';
1883
- out.channelStatusLine = line.trim();
1884
- if (/running|connected|ready|ok/i.test(line)) out.channelStatus = 'connected';
1885
- else if (/stopped|disconnected|error|failed/i.test(line)) out.channelStatus = 'disconnected';
1886
- else if (line) out.channelStatus = 'starting';
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 out;
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
- const env = buildEnvFileContent({ ...common, apiKey: '', botToken: '' });
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
- await recreateDockerBot(projectDir).catch(() => {});
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 ${pluginInstallSpec(id)} inside container ${botContainer}...`);
3346
+ sendLog(`[plugin] Installing/updating ${installSpec} inside container ${botContainer}...`);
3239
3347
 
3240
- const cmd = `cd /home/node/project && openclaw plugins install ${pluginInstallSpec(id)} --force`;
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 ${pluginInstallSpec(id)}...`);
3440
+ sendLog(`[plugin] Installing ${installSpec}...`);
3333
3441
 
3334
3442
  let installSuccess = true;
3335
- await run('openclaw', ['plugins', 'install', pluginInstallSpec(id), '--force'], {
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: 'Mặc định an toàn: DM theo chế pairing, group tắt hết sau khi quét QR hãy tự chọn DM/nhóm được phép trong cấu hình.', textEn: 'Secure defaults: DM uses pairing, all groups start disabled after the QR scan, explicitly choose the allowed DMs/groups in the config.' },
65
+ { textVi: 'Sau khi quét QR, bot nhận được tin nhắn xác nhận Owner ngay; bạn 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.0's strict channel schema.
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: 'pairing',
77
- allowFrom: [],
76
+ dmPolicy: 'open',
77
+ allowFrom: ['*'],
78
78
  groupPolicy: 'allowlist',
79
79
  groups: {
80
- '*': { enabled: false, requireMention: true },
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
- // Secure defaults: DM pairing, empty
314
- // allowFrom, all groups disabled + requireMention until the owner explicitly
315
- // enables them post-QR. Never default to `allowFrom: ["*"]` / open groups.
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: 'pairing',
340
- allowFrom: [],
338
+ dmPolicy: 'open',
339
+ allowFrom: ['*'],
341
340
  groupPolicy: 'allowlist',
342
341
  groups: {
343
- '*': { enabled: false, requireMention: true },
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.0';
48
- const ZALO_CONNECT_PLUGIN_SPEC = 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.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.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) ? `<button class="secondary btn-inline" data-zalo-login-trigger type="button" style="min-height: 36px; padding: 6px 12px; font-size: 13px;">🔑 ${t('Đăng nhập Zalo','Zalo Login')}</button>` : ''}
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
- 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')}">&times;</button></div><b>${escapeHtml(b.name)}</b><small title="${escapeHtml(role)}">${escapeHtml(role)}</small></article>`;
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')}">&times;</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 zaloHealthCard(channelBots = []) {
1515
- if (state.zaloBackend !== 'zalo-connect' || !channelBots.length) return '';
1516
- const h = state.zaloHealth;
1517
- const chip = (label, tone) => `<span class="runtime-badge ${tone}" style="font-size:11px;">${label}</span>`;
1518
- let statusChip = chip(t('Chưa rõ','Unknown'), 'warn');
1519
- if (h) {
1520
- if (h.channelStatus === 'connected') statusChip = chip(t('Đã kết nối','Connected'), 'ok');
1521
- else if (h.channelStatus === 'container-stopped') statusChip = chip(t('Container tắt','Container stopped'), 'bad');
1522
- else if (h.channelStatus === 'disconnected') statusChip = chip(t('Mất kết nối','Disconnected'), 'bad');
1523
- else if (h.channelStatus === 'starting') statusChip = chip(t('Đang khởi động','Starting'), 'warn');
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 = {};
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.12.0",
3
+ "version": "5.13.3",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {