create-openclaw-bot 5.12.0 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.0-0EA5E9?style=for-the-badge" alt="Version 5.13.0" /></a>
11
11
  <a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
12
12
  <a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
13
13
  <a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
@@ -27,12 +27,31 @@
27
27
 
28
28
  ---
29
29
 
30
- ## 🆕 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.13.0</b></summary>
31
48
 
32
49
  - 💬 **OpenClaw Zalo Connect, ready in one click**: create a personal Zalo bot or press **Zalo Login** and Setup prepares the channel automatically, then opens the QR flow in the dashboard.
33
50
  - ⚡ **Install once, reconnect faster**: an existing Zalo Connect installation is reused instead of being downloaded again on every login or restart.
34
51
  - ✨ **Cleaner native Zalo experience**: group mentions, reactions and moderation now run through one maintained Zalo runtime; the retired integration and Sticker add-on are gone from Setup.
35
52
 
53
+ </details>
54
+
36
55
  <details>
37
56
  <summary><b>Previous: What's new in v5.11.1</b></summary>
38
57
 
package/README.vi.md CHANGED
@@ -7,7 +7,7 @@
7
7
  *Chạy một lệnh → mở dashboard → bot lên sóng. Windows · macOS · Linux · VPS — chạy trên Docker, tự cài giúp bạn.*
8
8
 
9
9
  <p align="center">
10
- <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.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.0-0EA5E9?style=for-the-badge" alt="Version 5.13.0" /></a>
11
11
  <a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
12
12
  <a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
13
13
  <a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
@@ -27,12 +27,31 @@
27
27
 
28
28
  ---
29
29
 
30
- ## 🆕 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.13.0</b></summary>
31
48
 
32
49
  - 💬 **OpenClaw Zalo Connect, sẵn sàng trong một chạm**: tạo bot Zalo cá nhân hoặc bấm **Đăng nhập Zalo**, Setup tự chuẩn bị channel rồi mở luồng quét QR ngay trên dashboard.
33
50
  - ⚡ **Cài một lần, kết nối lại nhanh hơn**: nếu Zalo Connect đã có, Setup dùng lại bản hiện tại thay vì tải lại mỗi lần đăng nhập hay restart.
34
51
  - ✨ **Trải nghiệm Zalo native gọn hơn**: mention nhóm, reaction và quản trị dùng chung một Zalo runtime được duy trì; luồng tích hợp cũ và tiện ích Sticker đã được gỡ khỏi Setup.
35
52
 
53
+ </details>
54
+
36
55
  <details>
37
56
  <summary><b>Trước đó: Có gì mới trong v5.11.1</b></summary>
38
57
 
@@ -1221,6 +1221,12 @@ function mapAgentChannels(agent, cfg) {
1221
1221
  return enabled.length === 1 ? enabled : [mapAgentChannel(agent, cfg)];
1222
1222
  }
1223
1223
 
1224
+ function bindingChannelId(channel = '') {
1225
+ if (channel === 'zalo-personal') return 'zalo-connect';
1226
+ if (channel === 'zalo-bot') return 'zalo';
1227
+ return channel;
1228
+ }
1229
+
1224
1230
  async function listConfiguredBots(projectDir) {
1225
1231
  const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
1226
1232
  if (!projectDir || !existsSync(cfgPath)) return [];
@@ -1234,11 +1240,14 @@ async function listConfiguredBots(projectDir) {
1234
1240
  const env = await readProjectEnv(projectDir);
1235
1241
  const hasOwnWorkspace = !!agent.workspace;
1236
1242
  const identityName = usableIdentityName(identity.name);
1237
- return mapAgentChannels(agent, cfg).map((channel) => ({
1243
+ return mapAgentChannels(agent, cfg).map((channel) => {
1244
+ const binding = (cfg.bindings || []).find((b) => b.agentId === agent.id && b.match?.channel === bindingChannelId(channel));
1245
+ return ({
1238
1246
  id: agent.id,
1239
1247
  name: (hasOwnWorkspace ? identityName : agent.name) || agent.name || identityName || agent.id,
1240
1248
  role: identity.role || meta.role || agent.role || agent.desc || agent.description || '',
1241
1249
  channel,
1250
+ accountId: binding?.match?.accountId || 'default',
1242
1251
  workspace: agent.workspace || `.openclaw/${workspaceRelForAgent(agent, cfg, projectDir)}`,
1243
1252
  agentDir: agent.agentDir,
1244
1253
  persona: meta.persona || '',
@@ -1250,7 +1259,8 @@ async function listConfiguredBots(projectDir) {
1250
1259
  pageAccessToken: channel === 'fb-messenger' ? (env.FB_MESSENGER_PAGE_ACCESS_TOKEN || '') : '',
1251
1260
  appSecret: channel === 'fb-messenger' ? (env.FB_MESSENGER_APP_SECRET || '') : '',
1252
1261
  verifyToken: channel === 'fb-messenger' ? (env.FB_MESSENGER_VERIFY_TOKEN || '') : '',
1253
- }));
1262
+ });
1263
+ });
1254
1264
  }));
1255
1265
  return rows.flat();
1256
1266
  }
@@ -1726,7 +1736,7 @@ async function startZaloConnectLogin(projectDir, accountId = 'default') {
1726
1736
  sendLog(`[zalo-connect] Plugin missing — installing pinned ${ZALO_CONNECT_PLUGIN_SPEC}...`);
1727
1737
  const repo = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[0];
1728
1738
  const ref = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[1] || ZALO_CONNECT_VERSION;
1729
- const installCmd = `tmp=/tmp/openclaw-plugin-zalo-connect-${ref}; rm -rf "$tmp"; git clone --depth 1 --branch "${ref}" "${repo}" "$tmp" && cd /home/node/project && openclaw plugins install "$tmp" 2>&1`;
1739
+ const installCmd = `tmp=/tmp/openclaw-plugin-zalo-connect-${ref}; rm -rf "$tmp"; git clone --depth 1 --branch "${ref}" "${repo}" "$tmp" && rm -rf "$tmp/.git" && cd /home/node/project && openclaw plugins install "$tmp" 2>&1`;
1730
1740
  const inst = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', installCmd], { cwd: projectDir, shell: false });
1731
1741
  const instOut = `${inst.stdout}\n${inst.stderr}`;
1732
1742
  for (const line of instOut.split(/\r?\n/).filter(Boolean)) sendLog(`[zalo-connect] ${line}`);
@@ -1823,72 +1833,146 @@ function cancelZaloLogin() {
1823
1833
  return { ok: true, cancelled: false };
1824
1834
  }
1825
1835
 
1826
- // ── 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.
1836
+ function buildZaloHealthSnapshot(cfg = {}, statusJson = null, credentialNames = null, options = {}) {
1837
+ const containerRunning = options.containerRunning !== false;
1838
+ const textStatus = String(options.textStatus || '');
1839
+ const agents = new Map((cfg.agents?.list || []).map((agent) => [String(agent.id), agent]));
1840
+ const bindings = (cfg.bindings || []).filter((binding) => binding.match?.channel === 'zalo-connect');
1841
+ const runtimeAccounts = Array.isArray(statusJson?.channelAccounts?.['zalo-connect'])
1842
+ ? statusJson.channelAccounts['zalo-connect']
1843
+ : [];
1844
+ const expected = [];
1845
+ const seen = new Set();
1846
+ for (const binding of bindings) {
1847
+ const accountId = String(binding.match?.accountId || 'default');
1848
+ if (seen.has(accountId)) continue;
1849
+ seen.add(accountId);
1850
+ expected.push({ accountId, agentId: String(binding.agentId || '') });
1851
+ }
1852
+ for (const runtime of runtimeAccounts) {
1853
+ const accountId = String(runtime?.accountId || 'default');
1854
+ if (seen.has(accountId)) continue;
1855
+ seen.add(accountId);
1856
+ expected.push({ accountId, agentId: '' });
1857
+ }
1858
+
1859
+ const credentialSet = Array.isArray(credentialNames)
1860
+ ? new Set(credentialNames.map((name) => String(name || '').toLowerCase()))
1861
+ : null;
1862
+ const fallbackLines = textStatus.split(/\r?\n/).filter((line) => /zalo[- ]connect/i.test(line));
1863
+ const accounts = expected.map(({ accountId, agentId }) => {
1864
+ const runtime = runtimeAccounts.find((item) => String(item?.accountId || 'default') === accountId) || null;
1865
+ const line = fallbackLines.find((item) => accountId === 'default'
1866
+ ? /\bdefault\b/i.test(item) || fallbackLines.length === 1
1867
+ : item.toLowerCase().includes(accountId.toLowerCase())) || '';
1868
+ const fallbackRunning = /running|connected|ready|\bok\b/i.test(line);
1869
+ const fallbackFailed = /stopped|disconnected|error|failed/i.test(line);
1870
+ const configured = runtime ? runtime.configured === true : !!line && !fallbackFailed;
1871
+ const running = runtime ? runtime.running === true : fallbackRunning;
1872
+ const lastError = runtime?.lastError || (fallbackFailed ? line.trim() : null);
1873
+ const credentialFile = accountId === 'default'
1874
+ ? 'zalo-connect-credentials.json'
1875
+ : `zalo-connect-credentials-${accountId}.json`;
1876
+ const fileSaved = credentialSet ? credentialSet.has(credentialFile.toLowerCase()) : false;
1877
+ const agent = agents.get(agentId);
1878
+ return {
1879
+ accountId,
1880
+ agentId,
1881
+ name: agent?.name || agentId || accountId,
1882
+ configured,
1883
+ running,
1884
+ lastError,
1885
+ sessionSaved: fileSaved || runtime?.configured === true,
1886
+ };
1887
+ });
1888
+
1889
+ const total = accounts.length;
1890
+ const running = accounts.filter((account) => account.running).length;
1891
+ const configured = accounts.filter((account) => account.configured).length;
1892
+ const failed = accounts.filter((account) => account.lastError || (!account.running && account.configured)).length;
1893
+ let channelStatus = 'unknown';
1894
+ if (!containerRunning) channelStatus = 'container-stopped';
1895
+ else if (total > 0 && running === total) channelStatus = 'connected';
1896
+ else if (running > 0) channelStatus = 'partial';
1897
+ else if (total > 0 && (failed > 0 || configured > 0)) channelStatus = 'disconnected';
1898
+ else if (statusJson || fallbackLines.length) channelStatus = 'starting';
1899
+
1900
+ return {
1901
+ backend: cfg.channels?.['zalo-connect']?.enabled ? 'zalo-connect' : '',
1902
+ containerRunning,
1903
+ channelStatus,
1904
+ channelStatusLine: fallbackLines.join(' | '),
1905
+ summary: { total, running, configured, failed },
1906
+ accounts,
1907
+ };
1908
+ }
1909
+
1910
+ // ── Zalo health snapshot for the dashboard ──────────────────────────────────────
1911
+ // Runtime JSON is authoritative and account-aware. Text parsing remains only as a
1912
+ // compatibility fallback for older OpenClaw builds.
1830
1913
  async function getZaloHealth(projectDir) {
1831
- const out = {
1832
- backend: '',
1914
+ const meta = {
1833
1915
  supportedVersion: ZALO_CONNECT_VERSION,
1834
1916
  installedVersion: null,
1835
- containerRunning: false,
1836
- channelStatus: 'unknown',
1837
- channelStatusLine: '',
1838
- sessionSaved: false,
1839
1917
  zaloModInstalled: false,
1840
- accountId: 'default',
1918
+ zaloModVersion: null,
1841
1919
  };
1842
- if (!projectDir) return out;
1920
+ if (!projectDir) return { ...buildZaloHealthSnapshot({}, null, null, { containerRunning: false }), ...meta };
1843
1921
  let cfg = null;
1844
1922
  try { cfg = JSON.parse(await fsp.readFile(join(projectDir, '.openclaw', 'openclaw.json'), 'utf8')); } catch {}
1845
- if (!cfg) return 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'])
1923
+ if (!cfg) return { ...buildZaloHealthSnapshot({}, null, null, { containerRunning: false }), ...meta };
1924
+ meta.zaloModInstalled = !!(cfg.plugins?.entries?.['zalo-mod'] || cfg.plugins?.entries?.['openclaw-zalo-mod'])
1851
1925
  || existsSync(join(projectDir, '.openclaw', 'extensions', 'zalo-mod'));
1926
+ meta.zaloModVersion = await getInstalledPluginVersion(projectDir, ['zalo-mod', 'openclaw-zalo-mod']) || null;
1927
+ if (meta.zaloModVersion) meta.zaloModInstalled = true;
1852
1928
 
1853
- // Installed zalo-connect version — extensions/ is bind-mounted on macOS/Linux; fall back
1854
- // to reading inside the container (Windows keeps extensions on a named volume).
1855
1929
  const botContainer = getBotContainerName(projectDir);
1856
- if (out.backend === 'zalo-connect') {
1930
+ if (cfg.channels?.['zalo-connect']?.enabled) {
1857
1931
  const manifestHost = join(projectDir, '.openclaw', 'extensions', 'zalo-connect', 'openclaw.plugin.json');
1858
1932
  try {
1859
- out.installedVersion = JSON.parse(await fsp.readFile(manifestHost, 'utf8')).version || null;
1933
+ meta.installedVersion = JSON.parse(await fsp.readFile(manifestHost, 'utf8')).version || null;
1860
1934
  } catch {
1861
1935
  try {
1862
1936
  const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'cat "${OPENCLAW_HOME:-/home/node/project/.openclaw}/extensions/zalo-connect/openclaw.plugin.json" 2>/dev/null'], { cwd: projectDir, shell: false, timeout: 8000 });
1863
- out.installedVersion = JSON.parse(String(r.stdout || '{}')).version || null;
1937
+ meta.installedVersion = JSON.parse(String(r.stdout || '{}')).version || null;
1864
1938
  } catch {}
1865
1939
  }
1866
- // QR session/credentials saved? (zalo-connect stores its credential file under the state dir)
1867
- try {
1868
- const credRoot = join(projectDir, '.openclaw', 'credentials');
1869
- const names = existsSync(credRoot) ? await fsp.readdir(credRoot) : [];
1870
- out.sessionSaved = names.some((n) => n.toLowerCase().includes('zalo-connect'));
1871
- } catch {}
1872
1940
  }
1873
1941
 
1942
+ let containerRunning = false;
1874
1943
  try {
1875
1944
  const r = await runCapture('docker', ['inspect', '-f', '{{.State.Running}}', botContainer], { shell: false, timeout: 8000 });
1876
- out.containerRunning = String(r.stdout || '').trim() === 'true';
1945
+ containerRunning = String(r.stdout || '').trim() === 'true';
1877
1946
  } catch {}
1878
- if (out.containerRunning) {
1947
+ let statusJson = null;
1948
+ let textStatus = '';
1949
+ let credentialNames = null;
1950
+ if (containerRunning) {
1879
1951
  try {
1880
- const r = await runCapture('docker', ['exec', botContainer, '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';
1952
+ const r = await runCapture('docker', ['exec', botContainer, 'openclaw', 'channels', 'status', '--json'], { cwd: projectDir, shell: false, timeout: 20000 });
1953
+ statusJson = parseJsonText(String(r.stdout || '').trim(), null);
1954
+ } catch {}
1955
+ if (!statusJson) {
1956
+ try {
1957
+ const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'openclaw channels status 2>&1 || true'], { cwd: projectDir, shell: false, timeout: 20000 });
1958
+ textStatus = String(r.stdout || '');
1959
+ } catch {}
1960
+ }
1961
+ try {
1962
+ const script = "const fs=require('fs'),path=require('path'),os=require('os');const d=path.join(os.homedir(),'.openclaw');let a=[];try{a=fs.readdirSync(d).filter(n=>/^zalo-connect-credentials(?:-[^.]+)?\\.json$/i.test(n))}catch{}process.stdout.write(JSON.stringify(a))";
1963
+ const r = await runCapture('docker', ['exec', botContainer, 'node', '-e', script], { cwd: projectDir, shell: false, timeout: 8000 });
1964
+ credentialNames = parseJsonText(String(r.stdout || '[]').trim(), []);
1965
+ } catch {}
1966
+ try {
1967
+ const versions = await getContainerExtensionVersions(projectDir);
1968
+ const zaloModVersion = versions['zalo-mod'] || versions['openclaw-zalo-mod'] || '';
1969
+ if (zaloModVersion) {
1970
+ meta.zaloModInstalled = true;
1971
+ meta.zaloModVersion = zaloModVersion;
1972
+ }
1887
1973
  } catch {}
1888
- } else {
1889
- out.channelStatus = 'container-stopped';
1890
1974
  }
1891
- return out;
1975
+ return { ...buildZaloHealthSnapshot(cfg, statusJson, credentialNames, { containerRunning, textStatus }), ...meta };
1892
1976
  }
1893
1977
 
1894
1978
  function getBotServiceName(projectDir) {
@@ -2239,7 +2323,10 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
2239
2323
  const agentMetas = [];
2240
2324
  const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
2241
2325
  const cfg = buildOpenclawJson(common);
2242
- const env = buildEnvFileContent({ ...common, apiKey: '', botToken: '' });
2326
+ // A core project has no channel account yet. Keep its environment shared/credential-free;
2327
+ // writing the literal <your_bot_token> placeholder makes OpenClaw auto-detect Telegram and
2328
+ // emit a misleading "plugin not enabled" warning before the user has created any bot.
2329
+ const env = buildEnvFileContent({ ...common, apiKey: '', botToken: '', isSharedEnv: true });
2243
2330
  await fsp.writeFile(join(openclawHome, 'openclaw.json'), JSON.stringify(cfg, null, 2), 'utf8');
2244
2331
  await fsp.writeFile(join(projectDir, '.env'), env, 'utf8');
2245
2332
  await syncExecApprovals(projectDir, cfg);
@@ -2486,12 +2573,16 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
2486
2573
  await fsp.mkdir(dockerDir, { recursive: true });
2487
2574
  const envContent = existsSync(rootEnvPath)
2488
2575
  ? await fsp.readFile(rootEnvPath, 'utf8')
2489
- : buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
2576
+ : buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '', isSharedEnv: true });
2490
2577
  await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
2491
2578
  sendLog(`Docker env ready: ${dockerEnvPath}`);
2492
2579
  await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
2493
2580
  await applyResolved9RouterApiKey(projectDir).catch(() => {});
2494
- await recreateDockerBot(projectDir).catch(() => {});
2581
+ // The full compose start already created the bot container. Recreating it here used to
2582
+ // interrupt OpenClaw's first-boot state migration and leave its five-minute lease behind,
2583
+ // making a brand-new project appear to crash-loop until the lease expired. The config is
2584
+ // bind-mounted, so the resolved 9Router key does not require an immediate second recreate.
2585
+ probeCacheClear();
2495
2586
  }
2496
2587
  state.installed = true;
2497
2588
  sendLog('✅ Install completed');
@@ -3226,6 +3317,10 @@ async function installFeature(projectDir, agentId, kind, id) {
3226
3317
  }
3227
3318
 
3228
3319
  if (kind === 'plugin') {
3320
+ const installSpec = pluginInstallSpec(id);
3321
+ const installArgs = ['plugins', 'install', installSpec, '--force'];
3322
+ if (installSpec.startsWith('clawhub:')) installArgs.push('--acknowledge-clawhub-risk');
3323
+
3229
3324
  let composeDir = null;
3230
3325
  if (existsSync(join(projectDir, 'docker-compose.yml'))) {
3231
3326
  composeDir = projectDir;
@@ -3235,9 +3330,9 @@ async function installFeature(projectDir, agentId, kind, id) {
3235
3330
 
3236
3331
  if (composeDir) {
3237
3332
  const botContainer = getBotContainerName(projectDir);
3238
- sendLog(`[plugin] Installing/updating ${pluginInstallSpec(id)} inside container ${botContainer}...`);
3333
+ sendLog(`[plugin] Installing/updating ${installSpec} inside container ${botContainer}...`);
3239
3334
 
3240
- const cmd = `cd /home/node/project && openclaw plugins install ${pluginInstallSpec(id)} --force`;
3335
+ const cmd = `cd /home/node/project && openclaw ${installArgs.join(' ')}`;
3241
3336
  const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
3242
3337
 
3243
3338
  if (cmdOut) {
@@ -3329,10 +3424,10 @@ async function installFeature(projectDir, agentId, kind, id) {
3329
3424
  } else {
3330
3425
  // Fix any legacy config issues first
3331
3426
  await run('openclaw', ['doctor', '--fix'], { cwd: projectDir, env: openclawProjectEnv(projectDir) }).catch((err) => sendLog(`[plugin] doctor --fix skipped: ${err.message}`));
3332
- sendLog(`[plugin] Installing ${pluginInstallSpec(id)}...`);
3427
+ sendLog(`[plugin] Installing ${installSpec}...`);
3333
3428
 
3334
3429
  let installSuccess = true;
3335
- await run('openclaw', ['plugins', 'install', pluginInstallSpec(id), '--force'], {
3430
+ await run('openclaw', installArgs, {
3336
3431
  cwd: projectDir,
3337
3432
  env: openclawProjectEnv(projectDir),
3338
3433
  resolveOnPattern: /Installed plugin:/
@@ -4115,4 +4210,4 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
4115
4210
  printRemoteAccessHint(port).catch(() => {});
4116
4211
  }
4117
4212
 
4118
- export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder };
4213
+ export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, buildZaloHealthSnapshot };
@@ -197,7 +197,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
197
197
  function buildDockerArtifacts(options) {
198
198
  const {
199
199
  openClawNpmSpec,
200
- openClawRuntimePackages,
200
+ openClawRuntimePackages = '',
201
201
  is9Router,
202
202
  isLocal,
203
203
  isMultiBot,
@@ -287,6 +287,9 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
287
287
  ' tmp="/tmp/openclaw-plugin-$id-$ref"',
288
288
  ' rm -rf "$tmp"',
289
289
  ' if git clone --depth 1 --branch "$ref" "$repo" "$tmp" 2>/dev/null; then',
290
+ ' # Docker Desktop bind mounts can reject chmod on packed .git objects (EACCES).',
291
+ ' # OpenClaw only needs the plugin payload, never the clone metadata.',
292
+ ' rm -rf "$tmp/.git"',
290
293
  ' openclaw plugins install "$tmp" 2>/dev/null || echo "[entrypoint] warning: failed to install cloned plugin $id"',
291
294
  ' else',
292
295
  ' echo "[entrypoint] warning: failed to clone plugin $spec"',
package/dist/web/app.js CHANGED
@@ -799,14 +799,13 @@ function botView() {
799
799
  <div class="bot-main-content" style="margin-top: 14px;">
800
800
  <div class="card-head" style="margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center;">
801
801
  <h3 style="margin: 0; font-size: 16px;">${t('Danh sách bot','Bot list')}</h3>
802
- ${(ch === 'zalo-personal' && channelBots.length > 0) ? `<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,14 @@ function botCreateModal() {
896
897
  function botListPanel(bots) {
897
898
  const listItems = bots.map(b => {
898
899
  const role = (b.role || b.desc || b.description || '').trim() || t('Tr\u1ee3 l\u00fd OpenClaw','OpenClaw assistant');
899
- 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 quickBadge = health?.running ? `<span class="zalo-quick-badge">${t('Đã kết nối','Connected')}</span>` : '';
903
+ const connection = !health ? t('Chưa rõ','Unknown') : health.running ? t('Hoạt động','Active') : health.lastError ? t('Mất kết nối','Disconnected') : t('Đang kết nối','Connecting');
904
+ const login = !health ? t('Chưa rõ','Unknown') : health.sessionSaved ? t('Đã đăng nhập','Logged in') : t('Chưa đăng nhập','Not logged in');
905
+ const connectionTone = health?.running ? 'ok' : health?.lastError ? 'bad' : 'warn';
906
+ const loginTone = health?.sessionSaved ? 'ok' : health ? 'bad' : 'warn';
907
+ return `<article class="bot-item ${state.activeBotId===b.id?'active':''}" data-bot-id="${escapeHtml(b.id)}"><div class="bot-item-actions"><button class="bot-edit" data-edit-bot="${escapeHtml(b.id)}" title="${t('Sửa bot','Edit bot')}" aria-label="${t('Sửa bot','Edit bot')}">${actionIcon('edit')}</button><button class="bot-delete" data-delete-bot="${escapeHtml(b.id)}" title="${t('X\u00f3a bot','Delete bot')}" aria-label="${t('X\u00f3a bot','Delete bot')}">&times;</button></div><div class="bot-item-title"><b>${escapeHtml(b.name)}</b>${quickBadge}</div><small title="${escapeHtml(role)}">${escapeHtml(role)}</small>${isZalo ? `<div class="zalo-bot-health"><div><span>${t('Kết nối','Connection')}</span><em class="${connectionTone}">${connection}</em></div><div><span>${t('Đăng nhập','Login')}</span><em class="${loginTone}">${login}</em></div></div>` : ''}</article>`;
900
908
  });
901
909
  listItems.push(`
902
910
  <article class="bot-item bot-create-card" data-bot-modal="open">
@@ -1511,33 +1519,17 @@ async function loadFiles(silent=false){
1511
1519
  if (!silent) render();
1512
1520
  }
1513
1521
  async function loadCatalog(silent=false){ state.catalog = await api('/api/catalog'); if (!silent) render(); }
1514
- function 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>`;
1522
+ function zaloAccountHealth(bot) {
1523
+ const accounts = state.zaloHealth?.accounts || [];
1524
+ return accounts.find((account) => account.agentId === bot?.id)
1525
+ || accounts.find((account) => account.accountId === (bot?.accountId || 'default'))
1526
+ || null;
1527
+ }
1528
+ function zaloToolbar(channelBots = []) {
1529
+ const active = channelBots.find((bot) => bot.id === state.activeBotId) || channelBots[0];
1530
+ const health = zaloAccountHealth(active);
1531
+ const loginLabel = health?.sessionSaved ? t('Đăng nhập lại','Log in again') : t('Đăng nhập Zalo','Zalo Login');
1532
+ return `<div class="zalo-toolbar"><button class="secondary btn-inline" data-zalo-health-refresh type="button">${actionIcon('refresh')}<span>${t('Làm mới','Refresh')}</span></button><button class="secondary btn-inline" data-zalo-login-trigger type="button">🔑 <span>${loginLabel}</span></button></div>`;
1541
1533
  }
1542
1534
  async function loadZaloHealth(silent=false){
1543
1535
  if (!activeProjectDir()) { state.zaloHealth = null; return; }
@@ -1553,7 +1545,7 @@ async function loadFeatureFlags(silent=false){
1553
1545
  state.featureInstalled = data.installed || {};
1554
1546
  state.featureVersions = data.versions || {};
1555
1547
  state.zaloBackend = data.zaloBackend || '';
1556
- if (state.zaloBackend === 'zalo-connect') loadZaloHealth(true).catch(() => {});
1548
+ if (state.zaloBackend === 'zalo-connect') await loadZaloHealth(true).catch(() => {});
1557
1549
  } catch (_) {
1558
1550
  state.featureFlags = {};
1559
1551
  state.featureInstalled = {};
@@ -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.0",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {