create-openclaw-bot 5.11.1 → 5.12.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,
@@ -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,20 @@ 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
+ ' openclaw plugins install "$tmp" 2>/dev/null || echo "[entrypoint] warning: failed to install cloned plugin $id"',
291
+ ' else',
292
+ ' echo "[entrypoint] warning: failed to clone plugin $spec"',
293
+ ' fi',
294
+ ' ;;',
295
+ ' *) openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec" ;;',
296
+ ' esac',
296
297
  '}',
297
298
  'ensure_skill() {',
298
299
  ' id="$1"',
@@ -311,52 +312,14 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
311
312
  // Restore config AFTER plugin installs (which may clobber openclaw.json)
312
313
  runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
313
314
  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`);
315
+ if (zaloBackend === 'zalo-connect') {
316
+ // Pinned ZaloConnect install (never `latest` — plan §4.1). ensure_plugin skips the
317
+ // install when extensions/zalo-connect already exists, so restarts never re-download
318
+ // and a reconnect never reinstalls. No dist patching, no mentions.js, no watchdog
319
+ // 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';
321
+ runtimeParts.push(`ensure_plugin zalo-connect "${zaloConnectSpec}"`);
344
322
  }
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
323
  runtimeParts.push('openclaw gateway run');
361
324
  const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
362
325
  let browserInstall = '';
@@ -609,5 +572,3 @@ ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''}
609
572
  if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
610
573
  Object.assign(exports, globalThis.__openclawDockerGen);
611
574
  }
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>
@@ -806,6 +806,7 @@ function botView() {
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) : ''}
809
810
  </div>
810
811
  </section>
811
812
  <div class="bot-side-col">
@@ -999,7 +1000,6 @@ function botSkillsPanel() {
999
1000
  { id: 'cron', title: 'Cron', desc: 'Native scheduler (SQLite) — cron guide in TOOLS.md' },
1000
1001
  { id: 'image-gen', title: 'Tạo ảnh Infographic', desc: 'Tạo ảnh infographic, poster tự động qua 9Router' },
1001
1002
  { 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
1003
  { 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
1004
  ];
1005
1005
  const plugins = [
@@ -1015,12 +1015,14 @@ function botSkillsPanel() {
1015
1015
  // channels (e.g. Zalo helpers won't appear on Telegram/FB bots). Items without a
1016
1016
  // `channels` field are universal.
1017
1017
  const activeChannel = bot?.channel || state.botChannel || 'telegram';
1018
- const forChannel = (item) => !item.channels || item.channels.includes(activeChannel);
1018
+ const forChannel = (item) => {
1019
+ return !item.channels || item.channels.includes(activeChannel);
1020
+ };
1019
1021
  const scope = `${state.install?.projectDir || '-'} ? ${bot?.id || '-'}`;
1020
1022
  const row = (item, group) => {
1021
1023
  const key = `${group}:${item.id}`;
1022
1024
  const loading = !!state.featureLoading[key];
1023
- const requiresInstall = group === 'plugin' || (group === 'skill' && (item.id === 'image-gen' || item.id === 'sticker-mention' || item.id === 'learning-memory'));
1025
+ const requiresInstall = group === 'plugin' || (group === 'skill' && (item.id === 'image-gen' || item.id === 'learning-memory'));
1024
1026
  const isInstalled = !requiresInstall || !!state.featureInstalled?.[key];
1025
1027
 
1026
1028
  const version = requiresInstall && isInstalled ? (state.featureVersions?.[key] || '') : '';
@@ -1091,7 +1093,8 @@ function wireTab() {
1091
1093
  document.querySelectorAll('[data-fbplugin]').forEach(el => el.onclick = () => { state.fbPluginModalOpen = false; render(); });
1092
1094
  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
1095
  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(); });
1096
+ 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(); });
1097
+ document.querySelectorAll('[data-zalo-health-refresh]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { await loadZaloHealth(); }));
1095
1098
  document.querySelectorAll('[data-zalo-login-trigger]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
1096
1099
  state.zaloLoginOpen = true;
1097
1100
  state.zaloQrDataUrl = '';
@@ -1508,6 +1511,39 @@ async function loadFiles(silent=false){
1508
1511
  if (!silent) render();
1509
1512
  }
1510
1513
  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>`;
1541
+ }
1542
+ async function loadZaloHealth(silent=false){
1543
+ if (!activeProjectDir()) { state.zaloHealth = null; return; }
1544
+ try { state.zaloHealth = await api('/api/zalo/health' + projectQuery()); } catch (_) { state.zaloHealth = null; }
1545
+ if (!silent) render();
1546
+ }
1511
1547
  async function loadFeatureFlags(silent=false){
1512
1548
  const botId=currentBotId();
1513
1549
  if (!activeProjectDir()) { state.featureFlags = {}; state.featureInstalled = {}; state.featureVersions = {}; if (!silent) render(); return; }
@@ -1516,6 +1552,8 @@ async function loadFeatureFlags(silent=false){
1516
1552
  state.featureFlags = data.flags || {};
1517
1553
  state.featureInstalled = data.installed || {};
1518
1554
  state.featureVersions = data.versions || {};
1555
+ state.zaloBackend = data.zaloBackend || '';
1556
+ if (state.zaloBackend === 'zalo-connect') loadZaloHealth(true).catch(() => {});
1519
1557
  } catch (_) {
1520
1558
  state.featureFlags = {};
1521
1559
  state.featureInstalled = {};
@@ -1547,7 +1585,7 @@ function appendLogLine(line) {
1547
1585
  }, 2000);
1548
1586
  }
1549
1587
 
1550
- const qrMatch = String(line).match(/^\[zalouser:qr\]\s+(data:image\/[a-zA-Z0-9.+-]+;base64,\S+)/);
1588
+ const qrMatch = String(line).match(/^\[zalo-connect:qr\]\s+(data:image\/[a-zA-Z0-9.+-]+;base64,\S+)/);
1551
1589
  if (qrMatch) {
1552
1590
  state.zaloQrDataUrl = qrMatch[1];
1553
1591
  state.zaloLoginOpen = true;
@@ -1561,11 +1599,11 @@ function appendLogLine(line) {
1561
1599
  return;
1562
1600
  }
1563
1601
  const html = `<p>${escapeHtml(line)}</p>`;
1564
- if (state.zaloLoginOpen || /\[zalouser\]|zalo|qr|login|scan/i.test(line)) {
1602
+ if (state.zaloLoginOpen || /\[zalo-connect\]|zalo|qr|login|scan/i.test(line)) {
1565
1603
  state.zaloLoginLines.push(cleanTerminalLine(line));
1566
1604
  const qr = document.querySelector('[data-zalo-qr-log]');
1567
1605
  if (qr) { qr.textContent = state.zaloLoginLines.slice(-120).join('\n'); qr.scrollTop = qr.scrollHeight; }
1568
- if (/\[zalouser\].*scan.*qr/i.test(line) && state.zaloQrDataUrl) {
1606
+ if (/\[zalo-connect\].*scan.*qr/i.test(line) && state.zaloQrDataUrl) {
1569
1607
  render();
1570
1608
  requestAnimationFrame(() => document.querySelector('.zalo-login-modal')?.scrollTo({ top: 0, behavior: 'smooth' }));
1571
1609
  }
@@ -1629,6 +1667,3 @@ render();
1629
1667
  Promise.allSettled([loadSystem(true), loadStatus(true), loadCatalog(true)])
1630
1668
  .then(() => loadFeatureFlags(true).catch(() => {}))
1631
1669
  .finally(() => { render(); connectLogs(); });
1632
-
1633
-
1634
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.11.1",
3
+ "version": "5.12.0",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "build": "node docs_dev/tests/build.mjs --deploy",
14
14
  "dev": "node docs_dev/tests/build.mjs --deploy --watch",
15
15
  "start": "node dist/cli.js",
16
- "test": "node docs_dev/tests/smoke-cli-logic.mjs && node docs_dev/tests/test-generation.mjs && node docs_dev/tests/test-matrix.mjs && node docs_dev/tests/test-local-server.mjs",
16
+ "test": "node docs_dev/tests/smoke-cli-logic.mjs && node docs_dev/tests/test-generation.mjs && node docs_dev/tests/test-matrix.mjs && node docs_dev/tests/test-zalo-connect.mjs && node docs_dev/tests/test-local-server.mjs",
17
17
  "bump": "node docs_dev/tests/bump-version.mjs",
18
18
  "release": "node docs_dev/scripts/update.mjs"
19
19
  },