create-openclaw-bot 5.11.1 → 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.
@@ -55,31 +55,30 @@
55
55
  },
56
56
  // 'telegram+zalo-personal' — Combo mode tạm ngưng, nghiên cứu thêm.
57
57
  'zalo-personal': {
58
- name: 'Zalo Personal',
58
+ name: 'Zalo cá nhân — OpenClaw Zalo Connect',
59
59
  hasZaloPersonal: true,
60
60
  envKeys: [],
61
61
  envExtra: '',
62
62
  credSteps: [
63
- { textVi: '⚠️ Zalo Personal dùng <strong>unofficial API (zca-js)</strong> — chỉ nên dùng tài khoản phụ', textEn: '⚠️ Zalo Personal uses <strong>unofficial API (zca-js)</strong> — use an alternate account' },
64
- { textVi: 'Native setup sẽ tự chạy login copy QR về thư mục project. Nếu cần chạy lại thủ công, dùng <code>openclaw channels login --channel zalouser --verbose</code>.', textEn: 'Native setup now auto-runs the login flow and copies the QR into the project folder. If needed, rerun it manually with <code>openclaw channels login --channel zalouser --verbose</code>.' },
63
+ { textVi: '⚠️ Zalo 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
+ { 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 cơ 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
66
  ],
67
+ // Mirrors buildZaloConnectChannelConfig() in bot-config-gen.js — keys validated
68
+ // against OpenClaw Zalo Connect 3.0.0's strict channel schema.
66
69
  channelConfig: {
67
- zalouser: {
70
+ 'zalo-connect': {
68
71
  enabled: true,
69
72
  defaultAccount: 'default',
70
73
  accounts: {
71
- default: {
72
- dmPolicy: 'open',
73
- allowFrom: ['*'],
74
- groupPolicy: 'allowlist',
75
- groupAllowFrom: ['*'],
76
- },
74
+ default: { enabled: true },
77
75
  },
78
- dmPolicy: 'open',
79
- allowFrom: ['*'],
76
+ dmPolicy: 'pairing',
77
+ allowFrom: [],
80
78
  groupPolicy: 'allowlist',
81
- groupAllowFrom: ['*'],
82
- historyLimit: 50,
79
+ groups: {
80
+ '*': { enabled: false, requireMention: true },
81
+ },
83
82
  },
84
83
  },
85
84
  pluginInstall: '',
@@ -248,14 +248,14 @@
248
248
  });
249
249
  cfg.plugins = pluginsConfig.plugins;
250
250
 
251
- // ── bindings for zalouser ────────────────────────────────────────────────
251
+ // ── bindings for zalo-connect ────────────────────────────────────────────────
252
252
  if (agentMetas.length > 0 && isZaloPersonal(channelKey)) {
253
253
  cfg.bindings = cfg.bindings || [];
254
254
  const firstAgentId = agentMetas[0]?.agentId || 'bot';
255
- if (!cfg.bindings.some(b => b.match && b.match.channel === 'zalouser')) {
256
- // Account-specific from the start so a second Zalo account added later routes
257
- // correctly (each zalouser account binds by accountId).
258
- cfg.bindings.push({ agentId: firstAgentId, match: { channel: 'zalouser', accountId: 'default' } });
255
+ if (!cfg.bindings.some(b => b.match && b.match.channel === 'zalo-connect')) {
256
+ // Account-specific from day one (plan §5.2): each account needs its own
257
+ // binding + credential namespace even while zalo-connect 2.4.6 is single-account.
258
+ cfg.bindings.push({ agentId: firstAgentId, match: { channel: 'zalo-connect', accountId: 'default' } });
259
259
  }
260
260
  }
261
261
 
@@ -309,31 +309,15 @@
309
309
 
310
310
  channels.telegram = telegramConfig;
311
311
  } else if (isZaloPersonal(channelKey)) {
312
- // Zalo Personal matches live Mkt/Williams configs
313
- channels.zalouser = {
314
- enabled: true,
315
- defaultAccount: 'default',
316
- accounts: {
317
- default: {
318
- dmPolicy: 'open',
319
- allowFrom: ['*'],
320
- groupPolicy: 'allowlist',
321
- groupAllowFrom: ['*'],
322
- },
323
- },
324
- dmPolicy: 'open',
325
- allowFrom: ['*'],
326
- groupPolicy: 'allowlist',
327
- groupAllowFrom: ['*'],
328
- historyLimit: 50,
329
- // NOTE: do NOT add `reactionLevel`/`actions` here — the zalouser plugin's config
330
- // schema is strict and rejects them ("must not have additional properties"), which
331
- // crashes the gateway on boot. zalouser already supports the `react` action by
332
- // default; reaction behavior is driven by the prompt (TOOLS.md), not channel config.
333
- groups: {
334
- '*': { enabled: true, requireMention: false },
335
- },
336
- };
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
317
+ // (`additionalProperties: false`) — do NOT add keys (e.g. historyLimit,
318
+ // groupAllowFrom) without re-checking `openclaw.plugin.json` first, an unknown
319
+ // key crashes the gateway on boot.
320
+ channels['zalo-connect'] = buildZaloConnectChannelConfig();
337
321
  } else if (channelKey === 'zalo-bot') {
338
322
  channels.zalo = { enabled: true, provider: 'official_account' };
339
323
  }
@@ -342,6 +326,26 @@
342
326
  }
343
327
 
344
328
 
329
+ // ═══════════════════════════════════════════════════════════════════════════════
330
+ // buildZaloConnectChannelConfig — secure-default channels['zalo-connect'] block
331
+ // ═══════════════════════════════════════════════════════════════════════════════
332
+ function buildZaloConnectChannelConfig() {
333
+ return {
334
+ enabled: true,
335
+ defaultAccount: 'default',
336
+ accounts: {
337
+ default: { enabled: true },
338
+ },
339
+ dmPolicy: 'pairing',
340
+ allowFrom: [],
341
+ groupPolicy: 'allowlist',
342
+ groups: {
343
+ '*': { enabled: false, requireMention: true },
344
+ },
345
+ };
346
+ }
347
+
348
+
345
349
  // ═══════════════════════════════════════════════════════════════════════════════
346
350
  // buildPluginsConfig — returns { plugins: { ... } }
347
351
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -361,9 +365,11 @@
361
365
 
362
366
  const allow = ['memory-core'];
363
367
 
364
- // Zalo Personal channel is native; install openclaw-zalo-mod manually via ClawHub when needed.
368
+ // Zalo Personal → zalo-connect channel plugin (pinned install happens in the
369
+ // entrypoint / creation flow). openclaw-zalo-mod stays a separate opt-in plugin.
365
370
  if (isZaloPersonal(channelKey)) {
366
- allow.push('zalouser');
371
+ entries['zalo-connect'] = { enabled: true };
372
+ allow.push('zalo-connect');
367
373
  }
368
374
 
369
375
  // DuckDuckGo search plugin for web-search
@@ -518,6 +524,7 @@
518
524
  generateToken,
519
525
  buildOpenclawJson,
520
526
  buildChannelConfig,
527
+ buildZaloConnectChannelConfig,
521
528
  buildPluginsConfig,
522
529
  buildSkillsEntries,
523
530
  buildExecApprovalsJson,
@@ -40,6 +40,13 @@
40
40
  const TELEGRAM_RELAY_PLUGIN_ID = 'telegram-multibot-relay';
41
41
  const TELEGRAM_SETUP_GUIDE_FILENAME = 'TELEGRAM-GROUP-SETUP.md';
42
42
 
43
+ // ── OpenClaw Zalo Connect — single source of truth ──────────────────────────
44
+ // Pin the maintained fork tag so setup never falls back to upstream/ClawHub.
45
+ const ZALO_CHANNEL_ID = 'zalo-connect';
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';
49
+
43
50
  function buildRelayPluginInstallCommand(prefix = 'openclaw') {
44
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`;
45
52
  }
@@ -298,6 +305,10 @@ If setup reported a plugin install error, run this after the bot is running:
298
305
  TELEGRAM_RELAY_PLUGIN_SPEC,
299
306
  TELEGRAM_RELAY_PLUGIN_ID,
300
307
  TELEGRAM_SETUP_GUIDE_FILENAME,
308
+ ZALO_CHANNEL_ID,
309
+ ZALO_PLUGIN_ID,
310
+ ZALO_CONNECT_VERSION,
311
+ ZALO_CONNECT_PLUGIN_SPEC,
301
312
  buildRelayPluginInstallCommand,
302
313
  buildRelayPluginInstallCommandWin,
303
314
  buildTelegramPostInstallChecklist,
@@ -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,
@@ -223,6 +223,8 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
223
223
  gatewayPort = 18789,
224
224
  routerPort = 20128,
225
225
  osChoice = '',
226
+ // Personal-Zalo backend: empty or the single supported `zalo-connect` channel.
227
+ zaloBackend = '',
226
228
  } = options;
227
229
  // Windows bind-mounts give ClawHub-installed plugins world-writable perms (which openclaw
228
230
  // blocks), so on Windows we isolate extensions in a named volume. On macOS/Linux bind-mounts
@@ -278,21 +280,23 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
278
280
  ' return 0',
279
281
  ' fi',
280
282
  ' echo "[entrypoint] plugin $id missing; installing $spec"',
281
- ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
282
- '}',
283
- 'ensure_zalouser() {',
284
- ' NPM_DIR="$OPENCLAW_HOME/npm"',
285
- ' PKG_DIR="$NPM_DIR/node_modules/@openclaw/zalouser"',
286
- ' if [ -d "$PKG_DIR" ] || [ -d "$OPENCLAW_HOME/extensions/zalouser" ]; then',
287
- ' echo "[entrypoint] zalouser plugin already installed (npm or extensions) skipping"',
288
- ' else',
289
- ' echo "[entrypoint] zalouser plugin missing; installing via npm"',
290
- ' mkdir -p "$NPM_DIR"',
291
- ' cd "$NPM_DIR"',
292
- ' npm init -y 2>/dev/null || true',
293
- ' npm install @openclaw/zalouser@latest 2>/dev/null || echo "[entrypoint] warning: failed to install @openclaw/zalouser"',
294
- ' cd /home/node/project',
295
- ' fi',
283
+ ' case "$spec" in',
284
+ ' https://*.git#*)',
285
+ ' repo="${spec%%#*}"',
286
+ ' ref="${spec##*#}"',
287
+ ' tmp="/tmp/openclaw-plugin-$id-$ref"',
288
+ ' rm -rf "$tmp"',
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"',
293
+ ' openclaw plugins install "$tmp" 2>/dev/null || echo "[entrypoint] warning: failed to install cloned plugin $id"',
294
+ ' else',
295
+ ' echo "[entrypoint] warning: failed to clone plugin $spec"',
296
+ ' fi',
297
+ ' ;;',
298
+ ' *) openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec" ;;',
299
+ ' esac',
296
300
  '}',
297
301
  'ensure_skill() {',
298
302
  ' id="$1"',
@@ -311,52 +315,14 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
311
315
  // Restore config AFTER plugin installs (which may clobber openclaw.json)
312
316
  runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
313
317
  runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
314
- // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
315
- runtimeParts.push([
316
- '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
317
- 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
318
- 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
319
- ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
320
- ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
321
- 'fi',
322
- '# Automatically run Zalo sticker-mention patch if skill is present',
323
- 'for MENTIONS_JS in $(find "$OPENCLAW_HOME" -maxdepth 5 -path "*/skills/*/mentions.js" -type f 2>/dev/null); do',
324
- ' if [ -f "$MENTIONS_JS" ]; then',
325
- ' echo "[entrypoint] Running patch: $MENTIONS_JS"',
326
- ' node "$MENTIONS_JS" || echo "[entrypoint] Warning: failed to run patch $MENTIONS_JS"',
327
- ' fi',
328
- 'done',
329
- ].join('\n'));
330
- // Expose zalouser's ZCA API map (globalThis.__zcaApiByProfile) BEFORE the gateway imports
331
- // zalouser, so openclaw-zalo-mod can reach the live Zalo API (dashboard Sync Account, group
332
- // admin lookups, …). zalo-mod's own runtime patch lands AFTER zalouser is already imported,
333
- // so it never takes effect on the running module — patching here (post sticker-mention restore,
334
- // pre gateway-run) is the reliable fix.
335
- {
336
- const exposeZcaScript = [
337
- "const fs=require('fs'),cp=require('child_process');",
338
- "let files=[];",
339
- "try{ files=cp.execSync('find \"'+(process.env.OPENCLAW_HOME||'')+'/npm\" -path \"*/@openclaw/zalouser/dist/zalo-js-*.js\" -type f 2>/dev/null',{encoding:'utf8'}).split('\\n').filter(Boolean); }catch(e){}",
340
- "const reps=[['const apiByProfile = /* @__PURE__ */ new Map();','const apiByProfile = globalThis.__zcaApiByProfile || (globalThis.__zcaApiByProfile = /* @__PURE__ */ new Map());'],['const apiByProfile = new Map();','const apiByProfile = globalThis.__zcaApiByProfile || (globalThis.__zcaApiByProfile = new Map());']];",
341
- "for(const f of files){ try{ let s=fs.readFileSync(f,'utf8'); if(s.includes('globalThis.__zcaApiByProfile')) continue; for(const pair of reps){ if(s.includes(pair[0])){ s=s.replace(pair[0],pair[1]); fs.writeFileSync(f,s); console.log('[entrypoint] exposed __zcaApiByProfile for zalo-mod in '+f); break; } } }catch(e){} }",
342
- ].join('\n');
343
- runtimeParts.push(`# Expose zalouser ZCA API map for openclaw-zalo-mod (before gateway imports zalouser)\nnode - <<'NODE'\n${exposeZcaScript}\nNODE`);
318
+ if (zaloBackend === 'zalo-connect') {
319
+ // Pinned ZaloConnect install (never `latest` — plan §4.1). ensure_plugin skips the
320
+ // install when extensions/zalo-connect already exists, so restarts never re-download
321
+ // and a reconnect never reinstalls. No dist patching, no mentions.js, no watchdog
322
+ // hacks: sticker/mention/reaction are native ZaloConnect actions.
323
+ const zaloConnectSpec = common.ZALO_CONNECT_PLUGIN_SPEC || 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.0';
324
+ runtimeParts.push(`ensure_plugin zalo-connect "${zaloConnectSpec}"`);
344
325
  }
345
- runtimeParts.push([
346
- '# Zalo channel auto-restart monitor (background)',
347
- '(',
348
- ' sleep 180',
349
- ' while true; do',
350
- ' sleep 60',
351
- ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
352
- ' if echo "$STATUS" | grep -qi "stopped"; then',
353
- ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
354
- ' sleep 5',
355
- ' kill 1 2>/dev/null || true',
356
- ' fi',
357
- ' done',
358
- ') &',
359
- ].join('\n'));
360
326
  runtimeParts.push('openclaw gateway run');
361
327
  const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
362
328
  let browserInstall = '';
@@ -609,5 +575,3 @@ ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''}
609
575
  if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
610
576
  Object.assign(exports, globalThis.__openclawDockerGen);
611
577
  }
612
-
613
-
@@ -73,9 +73,9 @@ Notes:
73
73
  function buildZaloSoulSection(isVi, botName) {
74
74
  const name = botName || 'Bot';
75
75
  if (isVi) {
76
- return `\n\n**RULE — Zalo Group: Phản hồi theo chế độ Silent Mode:**\nKhi nhận tin từ \`channel: zalouser\` và \`group_id\` có giá trị:\n\n- Nếu tin nhắn chứa \`@${name}\` → **LUÔN reply** (bất kể silent mode).\n- Nếu tin nhắn bắt đầu bằng \`/\` (slash command) → KHÔNG reply, plugin đã xử lý rồi.\n- Tin thường trong group (không mention, không slash):\n - Nếu **Silent Mode BẬT** → tin này KHÔNG đến được bot (plugin đã chặn).\n - Nếu **Silent Mode TẮT** → tin này ĐẾN ĐƯỢC bot → **reply bình thường** như DM.\n- DM (không có group_id) → reply bình thường.`;
76
+ return `\n\n**RULE — Zalo Group: Phản hồi theo chế độ Silent Mode:**\nKhi nhận tin từ \`channel: zalo-connect\` và \`group_id\` có giá trị:\n\n- Nếu tin nhắn chứa \`@${name}\` → **LUÔN reply** (bất kể silent mode).\n- Nếu tin nhắn bắt đầu bằng \`/\` (slash command) → KHÔNG reply, plugin đã xử lý rồi.\n- Tin thường trong group (không mention, không slash):\n - Nếu **Silent Mode BẬT** → tin này KHÔNG đến được bot (plugin đã chặn).\n - Nếu **Silent Mode TẮT** → tin này ĐẾN ĐƯỢC bot → **reply bình thường** như DM.\n- DM (không có group_id) → reply bình thường.`;
77
77
  }
78
- return `\n\n**RULE — Zalo Group: Reply based on Silent Mode:**\nWhen receiving messages from \`channel: zalouser\` with a \`group_id\`:\n\n- If the message contains \`@${name}\` → **ALWAYS reply** (regardless of silent mode).\n- If the message starts with \`/\` (slash command) → DO NOT reply, the plugin already handled it.\n- Regular group messages (no mention, no slash):\n - If **Silent Mode is ON** → this message does NOT reach the bot (plugin blocks it).\n - If **Silent Mode is OFF** → this message DOES reach the bot → **reply normally** like DM.\n- DM (no group_id) → reply normally.`;
78
+ return `\n\n**RULE — Zalo Group: Reply based on Silent Mode:**\nWhen receiving messages from \`channel: zalo-connect\` with a \`group_id\`:\n\n- If the message contains \`@${name}\` → **ALWAYS reply** (regardless of silent mode).\n- If the message starts with \`/\` (slash command) → DO NOT reply, the plugin already handled it.\n- Regular group messages (no mention, no slash):\n - If **Silent Mode is ON** → this message does NOT reach the bot (plugin blocks it).\n - If **Silent Mode is OFF** → this message DOES reach the bot → **reply normally** like DM.\n- DM (no group_id) → reply normally.`;
79
79
  }
80
80
 
81
81
  // Structure mirrors OpenClaw's default SOUL.md (Core Truths / Boundaries / Vibe / Continuity);
@@ -547,7 +547,7 @@ _Good luck out there. Make it count._
547
547
  - [Operating Manual](./AGENTS.md)`;
548
548
  }
549
549
 
550
- function buildCronjobSkillMd(isVi = true) {
550
+ function buildCronjobSkillMd(isVi = true, zaloDeliveryChannel = 'zalo-connect') {
551
551
  return `---
552
552
  name: cronjob
553
553
  description: Lên lịch tác vụ định kỳ sử dụng công cụ cron.
@@ -571,7 +571,7 @@ Truyền tham số \`job\` (object) gồm:
571
571
  - \`message\`: Nội dung tin nhắn nhắc nhở.
572
572
  - **\`delivery\`**:
573
573
  - \`mode\`: \`"announce"\`.
574
- - \`channel\`: \`"zalouser"\`.
574
+ - \`channel\`: \`"${zaloDeliveryChannel}"\`.
575
575
  - \`to\`: ID người nhận hoặc ID nhóm.
576
576
  - ⚠️ **QUAN TRỌNG:** Nếu gửi tới Group Zalo, ID nhóm bắt buộc phải thêm tiền tố **\`g:\`** ở đầu (Ví dụ: \`g:1925989252066183028\`). Nếu thiếu \`g:\`, tin nhắn sẽ bị gửi nhầm thành tin cá nhân (DM) hoặc lỗi.
577
577
 
@@ -594,14 +594,6 @@ Truyền tham số \`job\` (object) gồm:
594
594
  return '';
595
595
  }
596
596
 
597
- function buildStickerMentionSkillMd(botName = 'Williams') {
598
- return '';
599
- }
600
-
601
- function buildStickerMentionJs() {
602
- return '';
603
- }
604
-
605
597
  function buildSecurityRules(isVi = true) {
606
598
  if (isVi) {
607
599
  return `\n\n## 🔐 Quy Tắc Bảo Mật — BẮT BUỘC (Red Lines)\n\n**GIỚI HẠN FILE & HỆ THỐNG:**\n- ✅ Được phép đọc/ghi trong: (1) workspace của bạn, và (2) các thư mục/ổ đĩa được CHỦ cấp quyền — mount tại \`/mnt/...\` (xem mục "💽 Thư mục/ổ đĩa được cấp quyền" ở trên nếu có). Mặc định quyền theo PROJECT: mọi bot dùng chung các mount này, trừ khi mục đó ghi giới hạn riêng cho từng bot.\n- ❌ KHÔNG truy cập file/thư mục NGOÀI workspace và các mount \`/mnt/...\` đã được cấp.\n- ❌ KHÔNG tiết lộ file nội bộ \`.openclaw\` (config.json, credentials, registry.json, token...).\n- ❌ KHÔNG tự ý quét/liệt kê thư mục hệ thống (Documents, Desktop, Downloads, AppData, registry, system32, Program Files) khi chưa được mount.\n- ❌ KHÔNG cài đặt phần mềm, driver, hoặc service ngoài Docker\n\n**API KEY & CREDENTIALS:**\n- ❌ KHÔNG BAO GIỜ hiển thị API key, token, hoặc mật khẩu trong chat\n- ❌ KHÔNG viết API key trực tiếp vào mã nguồn\n- ❌ KHÔNG commit file credentials lên Git\n- ✅ LUÔN lưu credentials trong file .env riêng\n- ✅ LUÔN dùng biến môi trường thay vì hardcode\n\n**VÍ CRYPTO & TÀI SẢN SỐ:**\n- ❌ TUYỆT ĐỐI KHÔNG truy cập, đọc, hoặc quét các thư mục ví crypto\n- ❌ KHÔNG quét clipboard (có thể chứa seed phrases)\n- ❌ KHÔNG truy cập browser profile, cookie, hoặc mật khẩu đã lưu\n- ❌ KHÔNG cài đặt npm package lạ (chỉ openclaw và plugin chính thức)\n\n**DOCKER:**\n- ✅ Việc cấp thêm ổ đĩa/thư mục cho bot là do CHỦ chủ động làm qua dashboard (nút "Cấp quyền ổ đĩa") — khi đó mount xuất hiện ở \`/mnt/...\` và bot ĐƯỢC PHÉP dùng. Đây là hợp lệ.\n- ❌ KHÔNG tự thêm/sửa mount trong docker-compose.yml; KHÔNG đề nghị mount nguyên ổ đĩa nếu chủ không yêu cầu.\n- ❌ KHÔNG chạy container với \`--privileged\`.\n- ✅ Chỉ expose cổng thật sự cần thiết.`;
@@ -1065,11 +1057,10 @@ This is a starting point. Add your own conventions, style, and rules as you figu
1065
1057
  hasBrowser = false,
1066
1058
  hasScheduler = false,
1067
1059
  hasZaloMod = false,
1068
- hasZaloSticker = false,
1069
1060
  browserDocVariant = '',
1070
1061
  } = options;
1071
1062
 
1072
- const isZalo = !!(hasZaloMod || hasZaloSticker);
1063
+ const isZalo = !!hasZaloMod;
1073
1064
 
1074
1065
  const frontmatter = isVi
1075
1066
  ? `---
@@ -1091,7 +1082,7 @@ description: Guide for utilizing tools and skills
1091
1082
 
1092
1083
  // Reaction guide (DM only). Detailed: lists the emoji palette + concrete examples + the
1093
1084
  // exact tool-call shape, to maximize the model's adherence (it's prompt-driven — there is
1094
- // no config gate for zalouser reactions).
1085
+ // no separate config gate for Zalo reactions).
1095
1086
  // Zalo reactions = EXACTLY the 6 native codes (Zalo renders them as icons). Telegram uses
1096
1087
  // unicode. `emoji` value sent to the react action MUST be one of these codes on Zalo.
1097
1088
  const reactList = isVi
@@ -1277,7 +1268,6 @@ Add whatever helps you do your job. This is your cheat sheet.
1277
1268
  * @property {boolean} [hasScheduler]
1278
1269
  * @property {boolean} [hasImageGen]
1279
1270
  * @property {boolean} [hasZaloMod]
1280
- * @property {boolean} [hasZaloSticker]
1281
1271
  */
1282
1272
 
1283
1273
  /**
@@ -1313,7 +1303,7 @@ Add whatever helps you do your job. This is your cheat sheet.
1313
1303
  hasScheduler = false,
1314
1304
  hasImageGen = false,
1315
1305
  hasZaloMod = false,
1316
- hasZaloSticker = false,
1306
+ zaloBackend = 'zalo-connect',
1317
1307
  } = opts;
1318
1308
 
1319
1309
  const isMultiBot = variant === 'relay';
@@ -1327,7 +1317,7 @@ Add whatever helps you do your job. This is your cheat sheet.
1327
1317
  }),
1328
1318
  'USER.md': buildUserDoc({ isVi, userInfo, variant: userVariant || (isMultiBot ? 'cli-multi' : 'wizard') }),
1329
1319
  'TOOLS.md': buildToolsDoc({
1330
- isVi, skillListStr, workspacePath, variant, agentWorkspaceDir, hasBrowser, hasScheduler, hasZaloMod, hasZaloSticker, browserDocVariant,
1320
+ isVi, skillListStr, workspacePath, variant, agentWorkspaceDir, hasBrowser, hasScheduler, hasZaloMod, browserDocVariant,
1331
1321
  }),
1332
1322
  'MEMORY.md': buildMemoryDoc({ isVi, variant: memoryVariant }),
1333
1323
  'HEARTBEAT.md': buildHeartbeatDoc({ isVi }),
@@ -1340,7 +1330,7 @@ Add whatever helps you do your job. This is your cheat sheet.
1340
1330
  }
1341
1331
 
1342
1332
  if (hasScheduler) {
1343
- files['skills/cronjob/SKILL.md'] = buildCronjobSkillMd(isVi);
1333
+ files['skills/cronjob/SKILL.md'] = buildCronjobSkillMd(isVi, 'zalo-connect');
1344
1334
  }
1345
1335
 
1346
1336
  if (hasImageGen) {
@@ -1348,11 +1338,6 @@ Add whatever helps you do your job. This is your cheat sheet.
1348
1338
  files['skills/infographic-generator/image-generator.js'] = buildInfographicGeneratorJs();
1349
1339
  }
1350
1340
 
1351
- if (hasZaloSticker) {
1352
- files['skills/sticker-mention/SKILL.md'] = buildStickerMentionSkillMd(botName);
1353
- files['skills/sticker-mention/mentions.js'] = buildStickerMentionJs();
1354
- }
1355
-
1356
1341
  return files;
1357
1342
  }
1358
1343
 
@@ -1372,8 +1357,6 @@ Add whatever helps you do your job. This is your cheat sheet.
1372
1357
  buildCronjobSkillMd,
1373
1358
  buildInfographicGeneratorSkillMd,
1374
1359
  buildInfographicGeneratorJs,
1375
- buildStickerMentionSkillMd,
1376
- buildStickerMentionJs,
1377
1360
  buildWorkspaceFileMap,
1378
1361
  };
1379
1362
 
package/dist/web/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const $ = (sel) => document.querySelector(sel);
2
- const state = { tab: 'dashboard', system: null, install: null, files: [], catalog: { skills: [], plugins: [] }, logs: [], zaloLoginOpen: false, zaloLoginLines: [], zaloQrDataUrl: '', lang: localStorage.getItem('openclaw-lang') || 'vi', theme: localStorage.getItem('openclaw-theme') || 'dark', navCollapsed: localStorage.getItem('openclaw-nav')==='1', os: null, mode: null, donateOpen: false, botModalOpen: false, botEditId: '', installModalOpen: false, fbPluginModalOpen: false, installTab: 'docker', installDraft: null, pathModal: null, confirmModal: null, botChannel: 'telegram', botPane: 'list', activeBotId: '', selectedFile: '', botMessage: '', projectConnectMessage: '', pendingProjectDir: '', selectedProjectDir: '', featureFlags: {}, featureInstalled: {}, featureLoading: {}, openDirs: {} };
2
+ const state = { tab: 'dashboard', system: null, install: null, files: [], catalog: { skills: [], plugins: [] }, logs: [], zaloLoginOpen: false, zaloLoginLines: [], zaloQrDataUrl: '', lang: localStorage.getItem('openclaw-lang') || 'vi', theme: localStorage.getItem('openclaw-theme') || 'dark', navCollapsed: localStorage.getItem('openclaw-nav')==='1', os: null, mode: null, donateOpen: false, botModalOpen: false, botEditId: '', installModalOpen: false, fbPluginModalOpen: false, installTab: 'docker', installDraft: null, pathModal: null, confirmModal: null, botChannel: 'telegram', botPane: 'list', activeBotId: '', selectedFile: '', botMessage: '', projectConnectMessage: '', pendingProjectDir: '', selectedProjectDir: '', featureFlags: {}, featureInstalled: {}, featureLoading: {}, zaloBackend: '', zaloHealth: null, openDirs: {} };
3
3
  const SVG_CDN = 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons';
4
4
  const OS_OPTIONS = [
5
5
  { id: 'win', title: 'Windows', subtitle: 'Auto-detected desktop', icon: `${SVG_CDN}/windows/default.svg`, badge: 'Desktop' },
@@ -13,7 +13,7 @@ const MODE_OPTIONS = [
13
13
  ];
14
14
  const BOT_CHANNELS = [
15
15
  { id: 'telegram', title: 'Telegram', subtitle: 'Bot API', icon: `${SVG_CDN}/telegram/default.svg`, badge: 'Tele' },
16
- { id: 'zalo-personal', title: 'Zalo User', subtitle: 'Personal account', icon: `${SVG_CDN}/zalo/default.svg`, badge: 'User' },
16
+ { id: 'zalo-personal', title: 'Zalo cá nhân', subtitle: 'OpenClaw Zalo Connect', icon: `${SVG_CDN}/zalo/default.svg`, badge: 'User' },
17
17
  { id: 'zalo-bot', title: 'Zalo API', subtitle: 'Official Account', icon: `${SVG_CDN}/zalo/default.svg`, badge: 'API' },
18
18
  { id: 'fb-messenger', title: 'Facebook', subtitle: 'Messenger', icon: `${SVG_CDN}/messenger/default.svg`, badge: 'FB' },
19
19
  { id: 'discord', title: 'Discord', subtitle: 'Bot', icon: `${SVG_CDN}/discord/default.svg`, badge: 'Discord', comingSoon: true },
@@ -194,7 +194,7 @@ function zaloLoginModal() {
194
194
  return `<div class="modal-backdrop zalo-login-backdrop" data-zalo-login="close">
195
195
  <section class="donate-modal zalo-login-modal" role="dialog" aria-modal="true" aria-label="Zalo login" onclick="event.stopPropagation()">
196
196
  <button class="modal-x" data-zalo-login="close" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
197
- <div class="donate-head"><span aria-hidden="true">Z</span><div><p>Zalo User</p><h2>${t('Quét mã QR đăng nhập','Scan login QR')}</h2><small>${t('Mở Zalo trên điện thoại → quét QR trong khung dưới. Nếu QR chưa hiện, đợi vài giây.', 'Open Zalo on your phone → scan the QR below. If it is not visible yet, wait a few seconds.')}</small></div></div>
197
+ <div class="donate-head"><span aria-hidden="true">Z</span><div><p>${t('Zalo cá nhân','Zalo personal')}</p><h2>${t('Quét mã QR đăng nhập','Scan login QR')}</h2><small>${t('Mở Zalo trên điện thoại → quét QR trong khung dưới. Nếu QR chưa hiện, đợi vài giây.', 'Open Zalo on your phone → scan the QR below. If it is not visible yet, wait a few seconds.')}</small></div></div>
198
198
  ${state.zaloQrDataUrl ? `<div class="zalo-qr-image-wrap"><img class="zalo-qr-image" src="${state.zaloQrDataUrl}" alt="Zalo login QR"/></div>` : ''}
199
199
  <pre class="zalo-qr-log" data-zalo-qr-log>${escapeHtml(lines)}</pre>
200
200
  <div class="zalo-login-actions"><button class="secondary" data-zalo-login="close" type="button">${t('\u0110óng','Close')}</button></div>
@@ -799,7 +799,7 @@ 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>`
@@ -825,6 +825,8 @@ function botView() {
825
825
  <div class="dash-version-list" style="margin-top: 18px;">
826
826
  <div><span>OpenClaw</span><b>${escapeHtml(openclawVer || '-')}</b></div>
827
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>` : ''}
828
830
  </div>
829
831
  </section>
830
832
  </div>
@@ -895,7 +897,14 @@ function botCreateModal() {
895
897
  function botListPanel(bots) {
896
898
  const listItems = bots.map(b => {
897
899
  const role = (b.role || b.desc || b.description || '').trim() || t('Tr\u1ee3 l\u00fd OpenClaw','OpenClaw assistant');
898
- 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>`;
899
908
  });
900
909
  listItems.push(`
901
910
  <article class="bot-item bot-create-card" data-bot-modal="open">
@@ -999,7 +1008,6 @@ function botSkillsPanel() {
999
1008
  { id: 'cron', title: 'Cron', desc: 'Native scheduler (SQLite) — cron guide in TOOLS.md' },
1000
1009
  { id: 'image-gen', title: 'Tạo ảnh Infographic', desc: 'Tạo ảnh infographic, poster tự động qua 9Router' },
1001
1010
  { id: 'web-search', title: 'Web Search', desc: 'Tìm kiếm web thời gian thực (DuckDuckGo)' },
1002
- { id: 'sticker-mention', title: 'Sticker & Auto-Tag (Zalo)', desc: 'Tự động tag người gửi và gửi sticker Zalo theo từ khóa', channels: ['zalo-personal'] },
1003
1011
  { id: 'learning-memory', title: 'Siêu Trí Nhớ Dài Hạn (learning-memory)', desc: 'Tự động ghi nhớ bài học vào MEMORY.md, tự đóng gói và tiến hóa kỹ năng mới vào skills/' },
1004
1012
  ];
1005
1013
  const plugins = [
@@ -1015,12 +1023,14 @@ function botSkillsPanel() {
1015
1023
  // channels (e.g. Zalo helpers won't appear on Telegram/FB bots). Items without a
1016
1024
  // `channels` field are universal.
1017
1025
  const activeChannel = bot?.channel || state.botChannel || 'telegram';
1018
- const forChannel = (item) => !item.channels || item.channels.includes(activeChannel);
1026
+ const forChannel = (item) => {
1027
+ return !item.channels || item.channels.includes(activeChannel);
1028
+ };
1019
1029
  const scope = `${state.install?.projectDir || '-'} ? ${bot?.id || '-'}`;
1020
1030
  const row = (item, group) => {
1021
1031
  const key = `${group}:${item.id}`;
1022
1032
  const loading = !!state.featureLoading[key];
1023
- const requiresInstall = group === 'plugin' || (group === 'skill' && (item.id === 'image-gen' || item.id === 'sticker-mention' || item.id === 'learning-memory'));
1033
+ const requiresInstall = group === 'plugin' || (group === 'skill' && (item.id === 'image-gen' || item.id === 'learning-memory'));
1024
1034
  const isInstalled = !requiresInstall || !!state.featureInstalled?.[key];
1025
1035
 
1026
1036
  const version = requiresInstall && isInstalled ? (state.featureVersions?.[key] || '') : '';
@@ -1091,7 +1101,8 @@ function wireTab() {
1091
1101
  document.querySelectorAll('[data-fbplugin]').forEach(el => el.onclick = () => { state.fbPluginModalOpen = false; render(); });
1092
1102
  document.querySelectorAll('[data-bot-modal]').forEach(el => el.onclick = () => { state.botModalOpen = el.dataset.botModal === 'open'; if (el.dataset.botModal === 'open') state.botEditId = ''; state.botMessage = ''; render(); });
1093
1103
  document.querySelectorAll('[data-edit-bot]').forEach(btn => btn.onclick = (ev) => { ev.stopPropagation(); state.botEditId = btn.dataset.editBot; state.botChannel = (state.install?.bots || []).find((b) => b.id === state.botEditId)?.channel || state.botChannel; state.botModalOpen = true; state.botMessage = ''; render(); });
1094
- document.querySelectorAll('[data-zalo-login]').forEach(el => el.onclick = () => { state.zaloLoginOpen = el.dataset.zaloLogin === 'open'; render(); });
1104
+ document.querySelectorAll('[data-zalo-login]').forEach(el => el.onclick = () => { const opening = el.dataset.zaloLogin === 'open'; if (!opening && state.zaloLoginOpen) api('/api/zalo/login/cancel', { method: 'POST' }).catch(() => {}); state.zaloLoginOpen = opening; render(); });
1105
+ document.querySelectorAll('[data-zalo-health-refresh]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { await loadZaloHealth(); }));
1095
1106
  document.querySelectorAll('[data-zalo-login-trigger]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
1096
1107
  state.zaloLoginOpen = true;
1097
1108
  state.zaloQrDataUrl = '';
@@ -1508,6 +1519,23 @@ async function loadFiles(silent=false){
1508
1519
  if (!silent) render();
1509
1520
  }
1510
1521
  async function loadCatalog(silent=false){ state.catalog = await api('/api/catalog'); if (!silent) render(); }
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>`;
1533
+ }
1534
+ async function loadZaloHealth(silent=false){
1535
+ if (!activeProjectDir()) { state.zaloHealth = null; return; }
1536
+ try { state.zaloHealth = await api('/api/zalo/health' + projectQuery()); } catch (_) { state.zaloHealth = null; }
1537
+ if (!silent) render();
1538
+ }
1511
1539
  async function loadFeatureFlags(silent=false){
1512
1540
  const botId=currentBotId();
1513
1541
  if (!activeProjectDir()) { state.featureFlags = {}; state.featureInstalled = {}; state.featureVersions = {}; if (!silent) render(); return; }
@@ -1516,6 +1544,8 @@ async function loadFeatureFlags(silent=false){
1516
1544
  state.featureFlags = data.flags || {};
1517
1545
  state.featureInstalled = data.installed || {};
1518
1546
  state.featureVersions = data.versions || {};
1547
+ state.zaloBackend = data.zaloBackend || '';
1548
+ if (state.zaloBackend === 'zalo-connect') await loadZaloHealth(true).catch(() => {});
1519
1549
  } catch (_) {
1520
1550
  state.featureFlags = {};
1521
1551
  state.featureInstalled = {};
@@ -1547,7 +1577,7 @@ function appendLogLine(line) {
1547
1577
  }, 2000);
1548
1578
  }
1549
1579
 
1550
- const qrMatch = String(line).match(/^\[zalouser:qr\]\s+(data:image\/[a-zA-Z0-9.+-]+;base64,\S+)/);
1580
+ const qrMatch = String(line).match(/^\[zalo-connect:qr\]\s+(data:image\/[a-zA-Z0-9.+-]+;base64,\S+)/);
1551
1581
  if (qrMatch) {
1552
1582
  state.zaloQrDataUrl = qrMatch[1];
1553
1583
  state.zaloLoginOpen = true;
@@ -1561,11 +1591,11 @@ function appendLogLine(line) {
1561
1591
  return;
1562
1592
  }
1563
1593
  const html = `<p>${escapeHtml(line)}</p>`;
1564
- if (state.zaloLoginOpen || /\[zalouser\]|zalo|qr|login|scan/i.test(line)) {
1594
+ if (state.zaloLoginOpen || /\[zalo-connect\]|zalo|qr|login|scan/i.test(line)) {
1565
1595
  state.zaloLoginLines.push(cleanTerminalLine(line));
1566
1596
  const qr = document.querySelector('[data-zalo-qr-log]');
1567
1597
  if (qr) { qr.textContent = state.zaloLoginLines.slice(-120).join('\n'); qr.scrollTop = qr.scrollHeight; }
1568
- if (/\[zalouser\].*scan.*qr/i.test(line) && state.zaloQrDataUrl) {
1598
+ if (/\[zalo-connect\].*scan.*qr/i.test(line) && state.zaloQrDataUrl) {
1569
1599
  render();
1570
1600
  requestAnimationFrame(() => document.querySelector('.zalo-login-modal')?.scrollTo({ top: 0, behavior: 'smooth' }));
1571
1601
  }
@@ -1629,6 +1659,3 @@ render();
1629
1659
  Promise.allSettled([loadSystem(true), loadStatus(true), loadCatalog(true)])
1630
1660
  .then(() => loadFeatureFlags(true).catch(() => {}))
1631
1661
  .finally(() => { render(); connectLogs(); });
1632
-
1633
-
1634
-
@@ -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 {