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.
- package/README.md +51 -36
- package/README.vi.md +51 -36
- package/dist/server/local-server.js +366 -360
- package/dist/setup/data/channels.js +13 -14
- package/dist/setup/shared/bot-config-gen.js +39 -32
- package/dist/setup/shared/common-gen.js +11 -0
- package/dist/setup/shared/docker-gen.js +27 -63
- package/dist/setup/shared/workspace-gen.js +9 -26
- package/dist/web/app.js +42 -15
- package/dist/web/styles.css +3 -0
- package/package.json +2 -2
|
@@ -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
|
|
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
|
|
64
|
-
{ textVi: '
|
|
63
|
+
{ textVi: '⚠️ Zalo cá nhân chạy qua <strong>OpenClaw Zalo Connect (unofficial, zca-js)</strong> — tự động hoá tài khoản cá nhân có thể vi phạm điều khoản Zalo và khiến tài khoản bị hạn chế. <strong>Chỉ nên dùng tài khoản phụ.</strong>', textEn: '⚠️ Personal Zalo runs on <strong>OpenClaw Zalo Connect (unofficial, zca-js)</strong> — automating a personal account may violate Zalo terms and can get the account restricted. <strong>Use a secondary account.</strong>' },
|
|
64
|
+
{ 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
|
-
|
|
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: '
|
|
79
|
-
allowFrom: [
|
|
76
|
+
dmPolicy: 'pairing',
|
|
77
|
+
allowFrom: [],
|
|
80
78
|
groupPolicy: 'allowlist',
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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 === '
|
|
256
|
-
// Account-specific from
|
|
257
|
-
//
|
|
258
|
-
cfg.bindings.push({ agentId: firstAgentId, match: { channel: '
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
'
|
|
282
|
-
'
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
'
|
|
286
|
-
'
|
|
287
|
-
'
|
|
288
|
-
'
|
|
289
|
-
'
|
|
290
|
-
'
|
|
291
|
-
'
|
|
292
|
-
'
|
|
293
|
-
'
|
|
294
|
-
'
|
|
295
|
-
'
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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\`: \`"
|
|
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 = !!
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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) ?
|
|
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
|
-
|
|
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')}">×</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) =>
|
|
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 === '
|
|
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 = () => {
|
|
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(/^\[
|
|
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 || /\[
|
|
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 (/\[
|
|
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
|
-
|
package/dist/web/styles.css
CHANGED
|
@@ -1790,6 +1790,9 @@ html[data-theme="light"] .bottom .nav.active {
|
|
|
1790
1790
|
text-align: right !important;
|
|
1791
1791
|
}
|
|
1792
1792
|
|
|
1793
|
+
/* Compact per-account Zalo health — shared versions stay in the right status column. */
|
|
1794
|
+
.zalo-toolbar{display:flex;gap:8px;align-items:center}.zalo-toolbar .btn-inline{min-height:36px;padding:6px 12px;font-size:13px}.zalo-toolbar svg{width:14px;height:14px}.bot-item-title{display:flex;align-items:center;gap:8px;min-width:0;padding-right:58px}.bot-item-title b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.zalo-quick-badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:999px;border:1px solid rgba(24,194,156,.35);background:rgba(24,194,156,.12);color:#25d6ad;font-size:10px;font-weight:800;white-space:nowrap}.zalo-bot-health{display:grid;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid rgba(255,255,255,.07)}.zalo-bot-health>div{display:flex;align-items:center;justify-content:space-between;gap:10px}.zalo-bot-health span{color:var(--muted);font-size:11px;font-weight:700}.zalo-bot-health em{font-size:11px;font-style:normal;font-weight:800;text-align:right}.zalo-bot-health em.ok{color:#25d6ad}.zalo-bot-health em.warn{color:#f7bf48}.zalo-bot-health em.bad{color:#ff6574}html[data-theme="light"] .zalo-bot-health{border-top-color:rgba(15,23,42,.08)}@media(max-width:640px){.zalo-toolbar{display:grid;grid-template-columns:1fr 1fr;width:100%}.zalo-toolbar .btn-inline{justify-content:center;padding:6px 8px}.bot-item-title{padding-right:54px}}
|
|
1795
|
+
|
|
1793
1796
|
/* Ensure bottom navigation bar is visible on tablet and mobile (under 1180px) */
|
|
1794
1797
|
@media (max-width: 1180px) {
|
|
1795
1798
|
.bottom {
|