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.
- package/README.md +32 -36
- package/README.vi.md +32 -36
- package/dist/server/local-server.js +264 -353
- 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 +23 -62
- package/dist/setup/shared/workspace-gen.js +9 -26
- package/dist/web/app.js +48 -13
- package/package.json +2 -2
|
@@ -13,10 +13,10 @@ function loadSharedModule(modulePath, globalName) {
|
|
|
13
13
|
if (loaded && Object.keys(loaded).length > 0) return loaded;
|
|
14
14
|
return globalThis[globalName] || loaded || {};
|
|
15
15
|
}
|
|
16
|
-
const { buildWorkspaceFileMap, buildCronjobSkillMd, buildInfographicGeneratorSkillMd, buildInfographicGeneratorJs
|
|
17
|
-
const { buildOpenclawJson, buildEnvFileContent, buildExecApprovalsJson } = loadSharedModule('../setup/shared/bot-config-gen.js', '__openclawBotConfig');
|
|
16
|
+
const { buildWorkspaceFileMap, buildCronjobSkillMd, buildInfographicGeneratorSkillMd, buildInfographicGeneratorJs } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
|
|
17
|
+
const { buildOpenclawJson, buildEnvFileContent, buildExecApprovalsJson, buildZaloConnectChannelConfig } = loadSharedModule('../setup/shared/bot-config-gen.js', '__openclawBotConfig');
|
|
18
18
|
const { buildDockerArtifacts } = loadSharedModule('../setup/shared/docker-gen.js', '__openclawDockerGen');
|
|
19
|
-
const { OPENCLAW_NPM_SPEC, NINE_ROUTER_NPM_SPEC, build9RouterProviderConfig, get9RouterBaseUrl } = loadSharedModule('../setup/shared/common-gen.js', '__openclawCommon');
|
|
19
|
+
const { OPENCLAW_NPM_SPEC, NINE_ROUTER_NPM_SPEC, ZALO_CHANNEL_ID, ZALO_PLUGIN_ID, ZALO_CONNECT_VERSION, ZALO_CONNECT_PLUGIN_SPEC, build9RouterProviderConfig, get9RouterBaseUrl } = loadSharedModule('../setup/shared/common-gen.js', '__openclawCommon');
|
|
20
20
|
const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
|
|
21
21
|
|
|
22
22
|
async function syncExecApprovals(projectDir, cfg) {
|
|
@@ -259,6 +259,7 @@ const STATE_FILE = '.openclaw-setup-state.json';
|
|
|
259
259
|
const DEFAULT_MODEL = 'smart-route';
|
|
260
260
|
const logClients = new Set();
|
|
261
261
|
let zaloLoginInFlight = false;
|
|
262
|
+
let zaloLoginChild = null; // active `openclaw channels login` process (for cancel)
|
|
262
263
|
let activeServerInstance = null;
|
|
263
264
|
// Captured at startup so a self-restart (update button) re-binds the SAME host/port,
|
|
264
265
|
// letting the browser tab reconnect to the new UI instead of hanging.
|
|
@@ -948,7 +949,7 @@ function ensureConfigShape(cfg) {
|
|
|
948
949
|
cfg.channels = cfg.channels || {};
|
|
949
950
|
cfg.bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
950
951
|
cfg.plugins = cfg.plugins || { entries: { 'memory-core': { config: { dreaming: { enabled: false } } } } };
|
|
951
|
-
// Preserve plugins.allow — needed for
|
|
952
|
+
// Preserve plugins.allow — needed for external plugins such as Zalo Connect.
|
|
952
953
|
if (!cfg.plugins.allow) cfg.plugins.allow = [];
|
|
953
954
|
cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
|
|
954
955
|
return cfg;
|
|
@@ -968,30 +969,19 @@ function ensureTelegramChannel(cfg) {
|
|
|
968
969
|
});
|
|
969
970
|
}
|
|
970
971
|
|
|
971
|
-
function
|
|
972
|
-
cfg
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
},
|
|
981
|
-
dmPolicy: 'open',
|
|
982
|
-
groupPolicy: 'open',
|
|
983
|
-
historyLimit: 50,
|
|
984
|
-
groups: {
|
|
985
|
-
'*': { enabled: true, requireMention: false },
|
|
986
|
-
},
|
|
987
|
-
allowFrom: ['*'],
|
|
988
|
-
groupAllowFrom: ['*'],
|
|
989
|
-
};
|
|
990
|
-
// Ensure zalouser is in plugins.entries (plugins.allow is deprecated)
|
|
972
|
+
function zaloBackendForConfig(cfg) {
|
|
973
|
+
return cfg?.channels?.['zalo-connect']?.enabled ? 'zalo-connect' : '';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function ensureZaloConnectChannel(cfg) {
|
|
977
|
+
// Secure defaults — DM pairing, no groups enabled
|
|
978
|
+
// until the owner picks them. Existing zalo-connect config is preserved as-is.
|
|
979
|
+
cfg.channels['zalo-connect'] = cfg.channels['zalo-connect'] || buildZaloConnectChannelConfig();
|
|
980
|
+
cfg.channels['zalo-connect'].enabled = true;
|
|
991
981
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
992
|
-
cfg.plugins.entries
|
|
982
|
+
cfg.plugins.entries['zalo-connect'] = cfg.plugins.entries['zalo-connect'] || { enabled: true };
|
|
993
983
|
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
994
|
-
if (!cfg.plugins.allow.includes('
|
|
984
|
+
if (!cfg.plugins.allow.includes('zalo-connect')) cfg.plugins.allow.push('zalo-connect');
|
|
995
985
|
}
|
|
996
986
|
|
|
997
987
|
function ensureFbMessengerChannel(cfg, pageId, appId) {
|
|
@@ -1208,7 +1198,7 @@ function mapAgentChannel(agent, cfg) {
|
|
|
1208
1198
|
if (agent && typeof agent === 'object' && ['telegram', 'zalo-personal', 'zalo-bot', 'fb-messenger'].includes(agent.channel)) return agent.channel;
|
|
1209
1199
|
const binding = (cfg.bindings || []).find((b) => b.agentId === agentId);
|
|
1210
1200
|
const ch = binding?.match?.channel;
|
|
1211
|
-
if (ch === '
|
|
1201
|
+
if (ch === 'zalo-connect') return 'zalo-personal';
|
|
1212
1202
|
if (ch === 'zalo') return 'zalo-bot';
|
|
1213
1203
|
if (ch === 'telegram') return 'telegram';
|
|
1214
1204
|
if (ch === 'fb-messenger') return 'fb-messenger';
|
|
@@ -1220,12 +1210,12 @@ function mapAgentChannels(agent, cfg) {
|
|
|
1220
1210
|
if (agent?.channel && ['telegram', 'zalo-personal', 'zalo-bot', 'fb-messenger'].includes(agent.channel)) return [agent.channel];
|
|
1221
1211
|
const channels = (cfg.bindings || [])
|
|
1222
1212
|
.filter((b) => b.agentId === agent?.id)
|
|
1223
|
-
.map((b) => b.match?.channel === '
|
|
1213
|
+
.map((b) => b.match?.channel === 'zalo-connect' ? 'zalo-personal' : b.match?.channel === 'zalo' ? 'zalo-bot' : b.match?.channel)
|
|
1224
1214
|
.filter((ch) => ['telegram', 'zalo-personal', 'zalo-bot', 'fb-messenger'].includes(ch));
|
|
1225
1215
|
if (channels.length) return Array.from(new Set(channels));
|
|
1226
1216
|
const enabled = [];
|
|
1227
1217
|
if (cfg.channels?.telegram?.enabled) enabled.push('telegram');
|
|
1228
|
-
if (cfg.channels?.
|
|
1218
|
+
if (cfg.channels?.['zalo-connect']?.enabled) enabled.push('zalo-personal');
|
|
1229
1219
|
if (cfg.channels?.zalo?.enabled) enabled.push('zalo-bot');
|
|
1230
1220
|
if (cfg.channels?.['fb-messenger']?.enabled) enabled.push('fb-messenger');
|
|
1231
1221
|
return enabled.length === 1 ? enabled : [mapAgentChannel(agent, cfg)];
|
|
@@ -1446,22 +1436,13 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1446
1436
|
cfg.bindings.push({ agentId, match: { channel: 'telegram', accountId } });
|
|
1447
1437
|
await appendEnvValue(projectDir, accountId === 'default' ? 'TELEGRAM_BOT_TOKEN' : `TELEGRAM_BOT_TOKEN_${agentId.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}`, token);
|
|
1448
1438
|
} else if (channel === 'zalo-personal') {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
const defAcct = zu.defaultAccount || 'default';
|
|
1453
|
-
// Make legacy catch-all zalouser bindings (no accountId) account-specific so a second
|
|
1454
|
-
// account isn't swallowed by them. The existing single bot owns the default account.
|
|
1455
|
-
for (const b of cfg.bindings) {
|
|
1456
|
-
if (b.match?.channel === 'zalouser' && !b.match.accountId) b.match.accountId = defAcct;
|
|
1439
|
+
const existingZaloConnectBindings = cfg.bindings.filter((b) => b.match?.channel === 'zalo-connect').length;
|
|
1440
|
+
if (existingZaloConnectBindings > 0) {
|
|
1441
|
+
throw httpError(400, 'OpenClaw Zalo Connect hiện hỗ trợ 1 tài khoản Zalo mỗi project. Hãy dùng bot Zalo hiện có hoặc tạo project riêng cho tài khoản thứ hai.');
|
|
1457
1442
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
accountId = existingZaloBindings === 0 ? defAcct : agentId;
|
|
1462
|
-
zu.enabled = true;
|
|
1463
|
-
zu.accounts[accountId] = zu.accounts[accountId] || { enabled: true, profile: accountId };
|
|
1464
|
-
cfg.bindings.push({ agentId, match: { channel: 'zalouser', accountId } });
|
|
1443
|
+
ensureZaloConnectChannel(cfg);
|
|
1444
|
+
accountId = 'default';
|
|
1445
|
+
cfg.bindings.push({ agentId, match: { channel: 'zalo-connect', accountId } });
|
|
1465
1446
|
} else if (channel === 'fb-messenger') {
|
|
1466
1447
|
// Token handling (user token → permanent Page token) is done by the fb-messenger
|
|
1467
1448
|
// plugin itself; here we just persist whatever the user supplied plus the App ID
|
|
@@ -1502,7 +1483,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1502
1483
|
workspacePath: `/home/node/project/.openclaw/${workspaceDir}`,
|
|
1503
1484
|
channel,
|
|
1504
1485
|
hasZaloMod: channel === 'zalo-personal',
|
|
1505
|
-
|
|
1486
|
+
zaloBackend: zaloBackendForConfig(cfg),
|
|
1506
1487
|
hasScheduler,
|
|
1507
1488
|
hasImageGen,
|
|
1508
1489
|
});
|
|
@@ -1558,8 +1539,8 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
1558
1539
|
cfg.channels.telegram.defaultAccount = cfg.channels.telegram.defaultAccount || 'default';
|
|
1559
1540
|
cfg.bindings.push({ agentId, match: { channel: 'telegram', accountId } });
|
|
1560
1541
|
} else if (channel === 'zalo-personal') {
|
|
1561
|
-
|
|
1562
|
-
cfg.bindings.push({ agentId, match: { channel: '
|
|
1542
|
+
ensureZaloConnectChannel(cfg);
|
|
1543
|
+
cfg.bindings.push({ agentId, match: { channel: 'zalo-connect', accountId: 'default' } });
|
|
1563
1544
|
} else if (channel === 'fb-messenger') {
|
|
1564
1545
|
ensureFbMessengerChannel(cfg, String(body.pageId || '').trim(), String(body.appId || '').trim());
|
|
1565
1546
|
cfg.bindings.push({ agentId, match: { channel: 'fb-messenger', accountId: 'default' } });
|
|
@@ -1624,7 +1605,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
1624
1605
|
workspacePath: `/home/node/project/.openclaw/${workspaceDir}`,
|
|
1625
1606
|
channel,
|
|
1626
1607
|
hasZaloMod: channel === 'zalo-personal',
|
|
1627
|
-
|
|
1608
|
+
zaloBackend: zaloBackendForConfig(cfg),
|
|
1628
1609
|
hasScheduler,
|
|
1629
1610
|
hasImageGen,
|
|
1630
1611
|
});
|
|
@@ -1668,7 +1649,7 @@ async function waitForDockerContainer(name, timeoutMs = 30000) {
|
|
|
1668
1649
|
return false;
|
|
1669
1650
|
}
|
|
1670
1651
|
|
|
1671
|
-
async function waitForGatewayZaloReady(botContainer, projectDir, timeoutMs = 90000) {
|
|
1652
|
+
async function waitForGatewayZaloReady(botContainer, projectDir, timeoutMs = 90000, channelKeywords = ['zalo-connect', 'openclaw zalo connect']) {
|
|
1672
1653
|
const started = Date.now();
|
|
1673
1654
|
// Use dynamic port from env: OPENCLAW_GATEWAY_PORT → OPENCLAW_PORT → fallback 18789
|
|
1674
1655
|
const healthScript = 'const http=require("http");const port=process.env.OPENCLAW_GATEWAY_PORT||process.env.OPENCLAW_PORT||18789;const r=http.get("http://127.0.0.1:"+port+"/health",{timeout:2000},(res)=>{let d="";res.on("data",c=>d+=c);res.on("end",()=>{try{const j=JSON.parse(d);process.stdout.write(j.ok?"READY":"WAIT")}catch{process.stdout.write("WAIT")}})});r.on("error",()=>process.stdout.write("WAIT"));r.on("timeout",()=>{r.destroy();process.stdout.write("WAIT")})';
|
|
@@ -1682,254 +1663,232 @@ async function waitForGatewayZaloReady(botContainer, projectDir, timeoutMs = 900
|
|
|
1682
1663
|
const status = String(out.stdout || '').trim();
|
|
1683
1664
|
if (status === 'READY') {
|
|
1684
1665
|
const pluginCheck = await runCapture('docker', ['exec', botContainer, 'sh', '-c', 'openclaw channels status 2>&1 || true'], { cwd: projectDir, shell: false });
|
|
1685
|
-
const output = (pluginCheck.stdout || '') + ' ' + (pluginCheck.stderr || '');
|
|
1686
|
-
if (
|
|
1666
|
+
const output = ((pluginCheck.stdout || '') + ' ' + (pluginCheck.stderr || '')).toLowerCase();
|
|
1667
|
+
if (channelKeywords.some((kw) => output.includes(kw))) {
|
|
1687
1668
|
ready = true;
|
|
1688
1669
|
break;
|
|
1689
1670
|
}
|
|
1690
|
-
if (attempts > 2) sendLog('[
|
|
1671
|
+
if (attempts > 2) sendLog('[zalo-connect] Gateway healthy but Zalo Connect is not loaded yet (' + Math.round((Date.now() - started) / 1000) + 's)...');
|
|
1691
1672
|
} else {
|
|
1692
|
-
if (attempts > 2 && attempts % 3 === 0) sendLog('[
|
|
1673
|
+
if (attempts > 2 && attempts % 3 === 0) sendLog('[zalo-connect] Waiting for gateway... (' + Math.round((Date.now() - started) / 1000) + 's)');
|
|
1693
1674
|
}
|
|
1694
1675
|
} catch {}
|
|
1695
1676
|
await new Promise((r) => setTimeout(r, 5000));
|
|
1696
1677
|
}
|
|
1697
1678
|
if (!ready) {
|
|
1698
|
-
sendLog('[
|
|
1679
|
+
sendLog('[zalo-connect] Gateway readiness timeout after ' + Math.round(timeoutMs / 1000) + 's — proceeding anyway.');
|
|
1699
1680
|
}
|
|
1700
1681
|
return ready;
|
|
1701
1682
|
}
|
|
1702
1683
|
|
|
1703
|
-
async function
|
|
1704
|
-
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
if (existsSync(cfgPath)) {
|
|
1722
|
-
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
1723
|
-
const binding = (cfg.bindings || []).find(b => b.agentId === state.activeBotId && b.match?.channel === 'zalouser');
|
|
1724
|
-
if (binding?.match?.accountId) {
|
|
1725
|
-
profile = binding.match.accountId;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
} catch (e) {}
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
const qrPaths = [
|
|
1732
|
-
`/tmp/openclaw/openclaw-zalouser-qr-${profile}.png`,
|
|
1733
|
-
`/tmp/openclaw-1000/openclaw-zalouser-qr-${profile}.png`
|
|
1734
|
-
];
|
|
1735
|
-
if (profile === 'default') {
|
|
1736
|
-
qrPaths.push(
|
|
1737
|
-
'/tmp/openclaw/openclaw-zalouser-qr.png',
|
|
1738
|
-
'/tmp/openclaw-1000/openclaw-zalouser-qr.png',
|
|
1739
|
-
'/tmp/openclaw/openclaw-zalouser-qr-default.png'
|
|
1740
|
-
);
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1684
|
+
async function startZaloLogin(projectDir, agentId = "") {
|
|
1685
|
+
const cfgPath = join(projectDir, ".openclaw", "openclaw.json");
|
|
1686
|
+
if (!existsSync(cfgPath)) throw httpError(404, "openclaw.json not found");
|
|
1687
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, "utf8"));
|
|
1688
|
+
const binding = (cfg.bindings || []).find((b) =>
|
|
1689
|
+
(!agentId || b.agentId === agentId) && b.match?.channel === "zalo-connect"
|
|
1690
|
+
);
|
|
1691
|
+
return startZaloConnectLogin(projectDir, binding?.match?.accountId || "default");
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// ── OpenClaw Zalo Connect QR login (new projects) ────────────────────────────────────────────
|
|
1695
|
+
// zalo-connect prints the QR two ways: ASCII art on stdout and a PNG written to the
|
|
1696
|
+
// container's tmpdir, announced with "QR image saved at: /tmp/zalo-connect-qr-<id>.png".
|
|
1697
|
+
// We watch stdout for that line, read the PNG out of the container, and push it to
|
|
1698
|
+
// the UI modal as a data URL ([zalo-connect:qr] log tag). Reconnect NEVER reinstalls the
|
|
1699
|
+
// plugin — install runs only when extensions/zalo-connect is absent, and always with the
|
|
1700
|
+
// pinned spec (never `latest`).
|
|
1701
|
+
async function startZaloConnectLogin(projectDir, accountId = 'default') {
|
|
1743
1702
|
if (zaloLoginInFlight) {
|
|
1744
|
-
setImmediate(async () => {
|
|
1745
|
-
try {
|
|
1746
|
-
const botContainer = getBotContainerName(projectDir);
|
|
1747
|
-
const js = `const fs=require('fs');const ps=${JSON.stringify(qrPaths)};for(const p of ps){try{if(fs.existsSync(p)&&fs.statSync(p).size>100){process.stdout.write(fs.readFileSync(p).toString('base64'));break;}}catch{}}`;
|
|
1748
|
-
const out = await runCapture('docker', ['exec', botContainer, 'node', '-e', js], { cwd: projectDir, shell: false });
|
|
1749
|
-
const b64 = extractCompletePngBase64(out.stdout);
|
|
1750
|
-
if (b64.length > 100) {
|
|
1751
|
-
sendLog(`[zalouser:qr] data:image/png;base64,${b64}`);
|
|
1752
|
-
sendLog('[zalouser] Found running login session. Displaying current QR code.');
|
|
1753
|
-
}
|
|
1754
|
-
} catch {}
|
|
1755
|
-
});
|
|
1756
1703
|
return { message: 'Zalo login is already running. Keep this modal open...' };
|
|
1757
1704
|
}
|
|
1758
|
-
zaloLoginInFlight = true;
|
|
1759
|
-
sendLog(`[zalouser] Preparing login for profile [${profile}]. QR will be generated for the UI modal.`);
|
|
1760
1705
|
const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
|
|
1761
|
-
if (
|
|
1762
|
-
|
|
1763
|
-
// Verify if zalouser is properly registered in installs.json with channels array.
|
|
1764
|
-
// npm install --prefix misses this, which causes error:not configured.
|
|
1765
|
-
const checkRegistryScript = `
|
|
1766
|
-
const fs = require('fs');
|
|
1767
|
-
try {
|
|
1768
|
-
// zalouser may live under npm/ (docker install) OR extensions/ (migrated/native). Treat
|
|
1769
|
-
// either as present so we never re-download it on top of an existing copy (that creates a
|
|
1770
|
-
// duplicate plugin id and breaks the shared ZCA API map).
|
|
1771
|
-
const distNpm = '/home/node/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist/index.js';
|
|
1772
|
-
const distExt = '/home/node/project/.openclaw/extensions/zalouser/dist/index.js';
|
|
1773
|
-
const inst = '/home/node/project/.openclaw/plugins/installs.json';
|
|
1774
|
-
if (!fs.existsSync(distNpm) && !fs.existsSync(distExt)) { console.log('MISSING'); process.exit(0); }
|
|
1775
|
-
if (!fs.existsSync(inst)) { console.log('MISSING_CHANNELS'); process.exit(0); }
|
|
1776
|
-
const j = JSON.parse(fs.readFileSync(inst, 'utf8'));
|
|
1777
|
-
const z = j.plugins.find(x => x.pluginId === 'zalouser');
|
|
1778
|
-
if (z && z.channels && z.channels.includes('zalouser')) {
|
|
1779
|
-
console.log('OK');
|
|
1780
|
-
} else {
|
|
1781
|
-
console.log('MISSING_CHANNELS');
|
|
1706
|
+
if (!existsSync(composeFile)) {
|
|
1707
|
+
throw httpError(400, 'Zalo login cần project Docker đang chạy (không tìm thấy docker-compose.yml).');
|
|
1782
1708
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
const
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
if(b.plugins.entries)c.plugins={...c.plugins,entries:b.plugins.entries};
|
|
1816
|
-
}
|
|
1817
|
-
if(!c.plugins)c.plugins={};if(!c.plugins.entries)c.plugins.entries={};
|
|
1818
|
-
if(!c.plugins.entries.zalouser)c.plugins.entries.zalouser={};
|
|
1819
|
-
c.plugins.entries.zalouser.enabled=true;
|
|
1820
|
-
fs.writeFileSync(cfg,JSON.stringify(c,null,2)+'\\n','utf8');
|
|
1821
|
-
fs.unlinkSync(bk);
|
|
1822
|
-
console.log('Config protected and restored.');
|
|
1823
|
-
}
|
|
1824
|
-
}catch(e){}
|
|
1825
|
-
try{
|
|
1826
|
-
console.log('Patching zalouser stability settings...');
|
|
1827
|
-
cp.execSync('ZALO_JS=$(find "/home/node/project/.openclaw" -path "*/zalouser/dist/zalo-js*.js" -type f 2>/dev/null | head -1); if [ -n "$ZALO_JS" ]; then sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS = 35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 120e3/g" "$ZALO_JS"; echo "Patched watchdog gap to 120s"; fi', {shell:true,stdio:'inherit'});
|
|
1828
|
-
}catch(e){}
|
|
1829
|
-
try{
|
|
1830
|
-
const ep = '/home/node/project/docker/openclaw/entrypoint.sh';
|
|
1831
|
-
if (fs.existsSync(ep)) {
|
|
1832
|
-
let content = fs.readFileSync(ep, 'utf8');
|
|
1833
|
-
if (!content.includes('zalo-monitor')) {
|
|
1834
|
-
const monitor = "\\n# Zalo channel auto-restart monitor (background)\\n(\\n sleep 180\\n while true; do\\n sleep 60\\n STATUS=$(openclaw channels status 2>/dev/null | grep -i \\"Zalo Personal\\" || true)\\n if echo \\"$STATUS\\" | grep -qi \\"stopped\\"; then\\n echo \\"[zalo-monitor] Zalo channel stopped - restarting container in 5s\\"\\n sleep 5\\n kill 1 2>/dev/null || true\\n fi\\n done\\n) &\\n";
|
|
1835
|
-
// Insert before 'openclaw gateway run'
|
|
1836
|
-
const target = 'openclaw gateway run';
|
|
1837
|
-
if (content.includes(target)) {
|
|
1838
|
-
content = content.replace(target, monitor + target);
|
|
1839
|
-
fs.writeFileSync(ep, content, 'utf8');
|
|
1840
|
-
console.log('Added auto-restart monitor to entrypoint.sh');
|
|
1709
|
+
zaloLoginInFlight = true;
|
|
1710
|
+
const botContainer = getBotContainerName(projectDir);
|
|
1711
|
+
sendLog(`[zalo-connect] Preparing QR login for account [${accountId}]...`);
|
|
1712
|
+
try {
|
|
1713
|
+
// NEVER poke the container while it is still booting: OpenClaw runs first-boot
|
|
1714
|
+
// migrations under a state lease, and a docker exec/restart mid-migration wedges
|
|
1715
|
+
// the lease and crash-loops the gateway. Wait for the container, then for the
|
|
1716
|
+
// gateway to report the zalo-connect channel (the entrypoint installs the pinned
|
|
1717
|
+
// plugin itself on first boot), and only fall back to an exec-install when the
|
|
1718
|
+
// gateway is up but the plugin is genuinely absent (projects created before the
|
|
1719
|
+
// backend-aware entrypoint existed).
|
|
1720
|
+
const containerUp = await waitForDockerContainer(botContainer, 90000);
|
|
1721
|
+
if (!containerUp) sendLog(`[zalo-connect] ${botContainer} chưa chạy sau 90s — vẫn thử tiếp...`);
|
|
1722
|
+
const gatewayReady = await waitForGatewayZaloReady(botContainer, projectDir, 180000, ['zalo-connect']);
|
|
1723
|
+
if (!gatewayReady) {
|
|
1724
|
+
const check = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', '[ -d "${OPENCLAW_HOME:-/home/node/project/.openclaw}/extensions/zalo-connect" ] && echo OK || echo MISSING'], { cwd: projectDir, shell: false }).catch(() => ({ stdout: 'ERR' }));
|
|
1725
|
+
if (String(check.stdout || '').trim() === 'MISSING') {
|
|
1726
|
+
sendLog(`[zalo-connect] Plugin missing — installing pinned ${ZALO_CONNECT_PLUGIN_SPEC}...`);
|
|
1727
|
+
const repo = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[0];
|
|
1728
|
+
const ref = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[1] || ZALO_CONNECT_VERSION;
|
|
1729
|
+
const installCmd = `tmp=/tmp/openclaw-plugin-zalo-connect-${ref}; rm -rf "$tmp"; git clone --depth 1 --branch "${ref}" "${repo}" "$tmp" && cd /home/node/project && openclaw plugins install "$tmp" 2>&1`;
|
|
1730
|
+
const inst = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', installCmd], { cwd: projectDir, shell: false });
|
|
1731
|
+
const instOut = `${inst.stdout}\n${inst.stderr}`;
|
|
1732
|
+
for (const line of instOut.split(/\r?\n/).filter(Boolean)) sendLog(`[zalo-connect] ${line}`);
|
|
1733
|
+
if (/installed plugin/i.test(instOut)) {
|
|
1734
|
+
// Gateway must reload to pick the plugin up — safe here: the gateway is past
|
|
1735
|
+
// its boot (we only reach this branch when it answered the exec above).
|
|
1736
|
+
await restartDockerBotContainer(projectDir).catch((err) => sendLog(`[docker] restart skipped/failed: ${err.message}`));
|
|
1737
|
+
await waitForGatewayZaloReady(botContainer, projectDir, 180000, ['zalo-connect']);
|
|
1738
|
+
} else {
|
|
1739
|
+
sendLog('[zalo-connect] Cài plugin không thành công — thử lại bằng nút "Đăng nhập Zalo" sau khi container ổn định.');
|
|
1740
|
+
}
|
|
1841
1741
|
}
|
|
1842
1742
|
}
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
zaloLoginInFlight = false;
|
|
1745
|
+
throw err;
|
|
1843
1746
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1747
|
+
|
|
1748
|
+
sendLog('[zalo-connect] Generating Zalo QR. The image will appear automatically.');
|
|
1749
|
+
const loginCmd = `cd /home/node/project && openclaw channels login --channel zalo-connect --account ${accountId} --verbose`;
|
|
1750
|
+
let qrSent = false;
|
|
1751
|
+
let loginDone = false;
|
|
1752
|
+
|
|
1753
|
+
const pushQrFromContainer = async (pngPath) => {
|
|
1754
|
+
const js = `const fs=require('fs');const p=${JSON.stringify(pngPath)};try{if(fs.existsSync(p)&&fs.statSync(p).size>100){process.stdout.write(fs.readFileSync(p).toString('base64'));}}catch{}`;
|
|
1755
|
+
const out = await runCapture('docker', ['exec', botContainer, 'node', '-e', js], { cwd: projectDir, shell: false }).catch(() => ({ stdout: '' }));
|
|
1756
|
+
const b64 = extractCompletePngBase64(out.stdout);
|
|
1757
|
+
if (b64.length > 100) {
|
|
1758
|
+
qrSent = true;
|
|
1759
|
+
sendLog(`[zalo-connect:qr] data:image/png;base64,${b64}`);
|
|
1760
|
+
sendLog('[zalo-connect] Scan this QR with the Zalo app.');
|
|
1858
1761
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const
|
|
1887
|
-
if (
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
sendLog(`[
|
|
1891
|
-
sendLog(
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
const isQrAsciiArt = (line) => /^[\s▀▄█▌▐░▒▓]+$/.test(line);
|
|
1765
|
+
const handleLine = (line) => {
|
|
1766
|
+
const qrFile = line.match(/QR image saved at:\s*(\S+\.png)/i);
|
|
1767
|
+
if (qrFile) {
|
|
1768
|
+
pushQrFromContainer(qrFile[1]).catch(() => {});
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (isQrAsciiArt(line)) return; // don't flood the UI modal with terminal QR art
|
|
1772
|
+
sendLog(`[zalo-connect] ${line}`);
|
|
1773
|
+
if (/login successful|login completed|logged in/i.test(line)) loginDone = true;
|
|
1774
|
+
};
|
|
1775
|
+
// Retry loop: right after a boot the gateway may still refuse `channels login`
|
|
1776
|
+
// ("container is restarting", "still preparing"), so give it a few spaced attempts.
|
|
1777
|
+
const MAX_ATTEMPTS = 3;
|
|
1778
|
+
const RETRY_DELAYS = [0, 10000, 20000];
|
|
1779
|
+
let attempt = 0;
|
|
1780
|
+
const runAttempt = () => {
|
|
1781
|
+
attempt++;
|
|
1782
|
+
if (attempt > 1) sendLog(`[zalo-connect] Retry ${attempt}/${MAX_ATTEMPTS}...`);
|
|
1783
|
+
const child = spawn('docker', ['exec', botContainer, 'sh', '-lc', loginCmd], { cwd: projectDir, shell: false, windowsHide: true });
|
|
1784
|
+
zaloLoginChild = child;
|
|
1785
|
+
child.stdout.on('data', (d) => String(d).split(/\r?\n/).filter(Boolean).forEach(handleLine));
|
|
1786
|
+
child.stderr.on('data', (d) => String(d).split(/\r?\n/).filter(Boolean).forEach(handleLine));
|
|
1787
|
+
child.on('error', (err) => sendLog(`[zalo-connect] Login process failed: ${err.message}`));
|
|
1788
|
+
child.on('close', async (code) => {
|
|
1789
|
+
const wasCancelled = child.killed && zaloLoginChild === null;
|
|
1790
|
+
if (zaloLoginChild === child) zaloLoginChild = null;
|
|
1791
|
+
sendLog(`[zalo-connect] Login process exited ${code}`);
|
|
1792
|
+
if (loginDone) {
|
|
1793
|
+
sendLog(`[zalo-connect] Login saved. Restarting ${botContainer} so the Zalo channel connects...`);
|
|
1794
|
+
await restartDockerBotContainer(projectDir).catch((err) => sendLog(`[zalo-connect] Container restart failed: ${err.message}`));
|
|
1795
|
+
sendLog(`[zalo-connect] ${botContainer} restarted. Try sending a Zalo message now.`);
|
|
1796
|
+
zaloLoginInFlight = false;
|
|
1797
|
+
} else if (code !== 0 && !qrSent && !wasCancelled && attempt < MAX_ATTEMPTS) {
|
|
1798
|
+
const delay = RETRY_DELAYS[attempt] || 15000;
|
|
1799
|
+
sendLog(`[zalo-connect] QR chưa sẵn sàng — thử lại sau ${delay / 1000}s...`);
|
|
1800
|
+
setTimeout(runAttempt, delay);
|
|
1801
|
+
} else {
|
|
1802
|
+
if (!qrSent && !wasCancelled) sendLog('[zalo-connect] Login ended without a QR. Click "Đăng nhập Zalo" to retry.');
|
|
1803
|
+
zaloLoginInFlight = false;
|
|
1892
1804
|
}
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
loginAttempt++;
|
|
1897
|
-
if (loginAttempt > 1) sendLog(`[zalouser] Retry attempt ${loginAttempt}/${MAX_LOGIN_ATTEMPTS}...`);
|
|
1898
|
-
const child = spawn('docker', ['exec', botContainer, 'sh', '-lc', loginCmd], { cwd: projectDir, shell: false, windowsHide: true });
|
|
1899
|
-
const handleLoginLine = (line) => {
|
|
1900
|
-
sendLog(line);
|
|
1901
|
-
if (/login successful|saved auth/i.test(line)) restartAfterLogin = true;
|
|
1902
|
-
};
|
|
1903
|
-
child.stdout.on('data', (d) => String(d).split(/\r?\n/).filter(Boolean).forEach(handleLoginLine));
|
|
1904
|
-
child.stderr.on('data', (d) => String(d).split(/\r?\n/).filter(Boolean).forEach(handleLoginLine));
|
|
1905
|
-
child.on('error', (err) => sendLog(`[zalouser] Login process failed: ${err.message}`));
|
|
1906
|
-
child.on('close', async (code) => {
|
|
1907
|
-
sendLog(`[zalouser] Login process exited ${code}`);
|
|
1908
|
-
if (code === 0 || restartAfterLogin || sent) {
|
|
1909
|
-
// Success or QR already found by poll
|
|
1910
|
-
if (restartAfterLogin) {
|
|
1911
|
-
sendLog(`[zalouser] Login saved. Restarting ${botContainer} container so Zalo User can receive messages...`);
|
|
1912
|
-
await restartDockerBotContainer(projectDir).catch((err) => sendLog(`[zalouser] Container restart failed: ${err.message}`));
|
|
1913
|
-
sendLog(`[zalouser] ${botContainer} restarted. Try sending a Zalo message now.`);
|
|
1914
|
-
}
|
|
1915
|
-
zaloLoginInFlight = false;
|
|
1916
|
-
} else if (loginAttempt < MAX_LOGIN_ATTEMPTS && !sent) {
|
|
1917
|
-
// Failed with "Still preparing QR" — retry after delay
|
|
1918
|
-
const delay = RETRY_DELAYS[loginAttempt] || 10000;
|
|
1919
|
-
sendLog(`[zalouser] QR not ready yet. Waiting ${delay / 1000}s before retry...`);
|
|
1920
|
-
setTimeout(runLoginAttempt, delay);
|
|
1921
|
-
} else {
|
|
1922
|
-
sendLog('[zalouser] All login attempts exhausted. Try clicking "Đăng nhập Zalo" again.');
|
|
1923
|
-
zaloLoginInFlight = false;
|
|
1924
|
-
}
|
|
1925
|
-
});
|
|
1926
|
-
};
|
|
1805
|
+
});
|
|
1806
|
+
};
|
|
1807
|
+
runAttempt();
|
|
1927
1808
|
|
|
1928
|
-
|
|
1929
|
-
|
|
1809
|
+
return { message: 'Generating Zalo QR. The image will appear automatically.' };
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Cancel an in-flight Zalo QR login (modal closed). Kills the CLI login process so
|
|
1813
|
+
// no orphaned login keeps polling Zalo; safe to call when nothing is running.
|
|
1814
|
+
function cancelZaloLogin() {
|
|
1815
|
+
const child = zaloLoginChild;
|
|
1816
|
+
if (child) {
|
|
1817
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
1818
|
+
zaloLoginChild = null;
|
|
1819
|
+
zaloLoginInFlight = false;
|
|
1820
|
+
sendLog('[zalo-connect] Login cancelled.');
|
|
1821
|
+
return { ok: true, cancelled: true };
|
|
1822
|
+
}
|
|
1823
|
+
return { ok: true, cancelled: false };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// ── Zalo health snapshot for the dashboard card ─────────────────────────────────
|
|
1827
|
+
// One cheap, non-throwing aggregate: backend, pinned vs installed version, channel
|
|
1828
|
+
// status (parsed from `openclaw channels status`), QR/session presence, Zalo Mod
|
|
1829
|
+
// presence. Everything degrades to null/'unknown' instead of failing the request.
|
|
1830
|
+
async function getZaloHealth(projectDir) {
|
|
1831
|
+
const out = {
|
|
1832
|
+
backend: '',
|
|
1833
|
+
supportedVersion: ZALO_CONNECT_VERSION,
|
|
1834
|
+
installedVersion: null,
|
|
1835
|
+
containerRunning: false,
|
|
1836
|
+
channelStatus: 'unknown',
|
|
1837
|
+
channelStatusLine: '',
|
|
1838
|
+
sessionSaved: false,
|
|
1839
|
+
zaloModInstalled: false,
|
|
1840
|
+
accountId: 'default',
|
|
1841
|
+
};
|
|
1842
|
+
if (!projectDir) return out;
|
|
1843
|
+
let cfg = null;
|
|
1844
|
+
try { cfg = JSON.parse(await fsp.readFile(join(projectDir, '.openclaw', 'openclaw.json'), 'utf8')); } catch {}
|
|
1845
|
+
if (!cfg) return out;
|
|
1846
|
+
if (cfg.channels?.['zalo-connect']?.enabled) out.backend = 'zalo-connect';
|
|
1847
|
+
if (!out.backend) return out;
|
|
1848
|
+
const binding = (cfg.bindings || []).find((b) => b.match?.channel === out.backend);
|
|
1849
|
+
if (binding?.match?.accountId) out.accountId = binding.match.accountId;
|
|
1850
|
+
out.zaloModInstalled = !!(cfg.plugins?.entries?.['zalo-mod'] || cfg.plugins?.entries?.['openclaw-zalo-mod'])
|
|
1851
|
+
|| existsSync(join(projectDir, '.openclaw', 'extensions', 'zalo-mod'));
|
|
1852
|
+
|
|
1853
|
+
// Installed zalo-connect version — extensions/ is bind-mounted on macOS/Linux; fall back
|
|
1854
|
+
// to reading inside the container (Windows keeps extensions on a named volume).
|
|
1855
|
+
const botContainer = getBotContainerName(projectDir);
|
|
1856
|
+
if (out.backend === 'zalo-connect') {
|
|
1857
|
+
const manifestHost = join(projectDir, '.openclaw', 'extensions', 'zalo-connect', 'openclaw.plugin.json');
|
|
1858
|
+
try {
|
|
1859
|
+
out.installedVersion = JSON.parse(await fsp.readFile(manifestHost, 'utf8')).version || null;
|
|
1860
|
+
} catch {
|
|
1861
|
+
try {
|
|
1862
|
+
const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'cat "${OPENCLAW_HOME:-/home/node/project/.openclaw}/extensions/zalo-connect/openclaw.plugin.json" 2>/dev/null'], { cwd: projectDir, shell: false, timeout: 8000 });
|
|
1863
|
+
out.installedVersion = JSON.parse(String(r.stdout || '{}')).version || null;
|
|
1864
|
+
} catch {}
|
|
1865
|
+
}
|
|
1866
|
+
// QR session/credentials saved? (zalo-connect stores its credential file under the state dir)
|
|
1867
|
+
try {
|
|
1868
|
+
const credRoot = join(projectDir, '.openclaw', 'credentials');
|
|
1869
|
+
const names = existsSync(credRoot) ? await fsp.readdir(credRoot) : [];
|
|
1870
|
+
out.sessionSaved = names.some((n) => n.toLowerCase().includes('zalo-connect'));
|
|
1871
|
+
} catch {}
|
|
1930
1872
|
}
|
|
1931
1873
|
|
|
1932
|
-
|
|
1874
|
+
try {
|
|
1875
|
+
const r = await runCapture('docker', ['inspect', '-f', '{{.State.Running}}', botContainer], { shell: false, timeout: 8000 });
|
|
1876
|
+
out.containerRunning = String(r.stdout || '').trim() === 'true';
|
|
1877
|
+
} catch {}
|
|
1878
|
+
if (out.containerRunning) {
|
|
1879
|
+
try {
|
|
1880
|
+
const r = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', 'openclaw channels status 2>&1 || true'], { cwd: projectDir, shell: false, timeout: 20000 });
|
|
1881
|
+
const needle = out.backend === 'zalo-connect' ? 'zalo-connect' : 'zalo';
|
|
1882
|
+
const line = String(r.stdout || '').split(/\r?\n/).find((l) => l.toLowerCase().includes(needle)) || '';
|
|
1883
|
+
out.channelStatusLine = line.trim();
|
|
1884
|
+
if (/running|connected|ready|ok/i.test(line)) out.channelStatus = 'connected';
|
|
1885
|
+
else if (/stopped|disconnected|error|failed/i.test(line)) out.channelStatus = 'disconnected';
|
|
1886
|
+
else if (line) out.channelStatus = 'starting';
|
|
1887
|
+
} catch {}
|
|
1888
|
+
} else {
|
|
1889
|
+
out.channelStatus = 'container-stopped';
|
|
1890
|
+
}
|
|
1891
|
+
return out;
|
|
1933
1892
|
}
|
|
1934
1893
|
|
|
1935
1894
|
function getBotServiceName(projectDir) {
|
|
@@ -1991,12 +1950,12 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1991
1950
|
const routerPort = state.routerPort || 20128;
|
|
1992
1951
|
const osChoice = await resolveProjectHostOs(projectDir);
|
|
1993
1952
|
|
|
1994
|
-
// Detect
|
|
1953
|
+
// Detect the single supported personal-Zalo backend from openclaw.json.
|
|
1995
1954
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
1996
|
-
let
|
|
1955
|
+
let zaloBackend = '';
|
|
1997
1956
|
try {
|
|
1998
1957
|
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
1999
|
-
|
|
1958
|
+
if (cfg.channels?.['zalo-connect']?.enabled) zaloBackend = 'zalo-connect';
|
|
2000
1959
|
} catch {}
|
|
2001
1960
|
|
|
2002
1961
|
// Regenerate with detected settings
|
|
@@ -2012,8 +1971,8 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
2012
1971
|
singleComposeName: composeName,
|
|
2013
1972
|
singleAppContainerName: botContainer,
|
|
2014
1973
|
singleRouterContainerName: routerContainer,
|
|
1974
|
+
zaloBackend,
|
|
2015
1975
|
runtimeCommandParts: [
|
|
2016
|
-
hasZalo ? 'ensure_zalouser' : '',
|
|
2017
1976
|
'while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done >/dev/null 2>&1 &',
|
|
2018
1977
|
].filter(Boolean),
|
|
2019
1978
|
plainSingleExtraHosts: true,
|
|
@@ -2089,37 +2048,6 @@ async function recreateDockerBot(projectDir) {
|
|
|
2089
2048
|
await run('docker', ['compose', '-f', composeFile, 'up', '-d', '--build', '--force-recreate', serviceName], { cwd: projectDir });
|
|
2090
2049
|
await waitForDockerContainer(containerName);
|
|
2091
2050
|
|
|
2092
|
-
// Automatically run Zalo sticker-mention patch if skill is enabled and agent is zalo-personal
|
|
2093
|
-
try {
|
|
2094
|
-
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
2095
|
-
if (existsSync(cfgPath)) {
|
|
2096
|
-
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
2097
|
-
const stickerMentionOn = !!cfg.skills?.entries?.['sticker-mention']?.enabled;
|
|
2098
|
-
if (stickerMentionOn) {
|
|
2099
|
-
for (const a of cfg.agents?.list || []) {
|
|
2100
|
-
const binding = (cfg.bindings || []).find((b) => b.agentId === a.id);
|
|
2101
|
-
const channel = binding?.match?.channel || 'telegram';
|
|
2102
|
-
if (channel === 'zalo-personal' || channel === 'zalouser') {
|
|
2103
|
-
const workspaceDir = workspaceRelForAgent(a, cfg, projectDir) || `workspace-${a.id}`;
|
|
2104
|
-
let mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/zalo-sticker-mention/mentions.js');
|
|
2105
|
-
let containerJsPath = `/home/node/project/.openclaw/${workspaceDir}/skills/zalo-sticker-mention/mentions.js`;
|
|
2106
|
-
if (!existsSync(mentionsJsPath)) {
|
|
2107
|
-
mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/sticker-mention/mentions.js');
|
|
2108
|
-
containerJsPath = `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`;
|
|
2109
|
-
}
|
|
2110
|
-
if (existsSync(mentionsJsPath)) {
|
|
2111
|
-
sendLog(`[zalo-patch] Automatically running mentions.js inside container ${containerName}...`);
|
|
2112
|
-
const patchCmd = await runCapture('docker', ['exec', containerName, 'node', containerJsPath], { cwd: projectDir, shell: false });
|
|
2113
|
-
sendLog(`[zalo-patch] Output: ${patchCmd.stdout || ''} ${patchCmd.stderr || ''}`);
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
} catch (err) {
|
|
2120
|
-
sendLog(`[zalo-patch] Failed to auto-run mentions.js: ${err.message}`);
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
2051
|
// Container was rebuilt/recreated: runtime, versions and extension versions may all have changed.
|
|
2124
2052
|
probeCacheClear();
|
|
2125
2053
|
return true;
|
|
@@ -3050,7 +2978,7 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
3050
2978
|
for (const a of cfg.agents.list) {
|
|
3051
2979
|
const sf = await readWorkspaceText(projectDir, a, 'skills/cronjob/SKILL.md');
|
|
3052
2980
|
await fsp.mkdir(dirname(sf.file), { recursive: true });
|
|
3053
|
-
await fsp.writeFile(sf.file, buildCronjobSkillMd(true), 'utf8');
|
|
2981
|
+
await fsp.writeFile(sf.file, buildCronjobSkillMd(true, 'zalo-connect'), 'utf8');
|
|
3054
2982
|
}
|
|
3055
2983
|
} else {
|
|
3056
2984
|
if (cfg.tools?.alsoAllow) cfg.tools.alsoAllow = cfg.tools.alsoAllow.filter((x) => x !== 'group:automation');
|
|
@@ -3072,47 +3000,22 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
3072
3000
|
}
|
|
3073
3001
|
}
|
|
3074
3002
|
|
|
3075
|
-
// Folder-based per-bot skills (image-gen /
|
|
3003
|
+
// Folder-based per-bot skills (image-gen / learning-memory). These load
|
|
3076
3004
|
// from <workspace>/skills, so each bot's copy is independent. Toggling here affects ONLY the
|
|
3077
3005
|
// active bot's workspace folder. The global skills.entries[id].enabled flag is kept true
|
|
3078
3006
|
// while ANY bot still has the folder (openclaw needs it true to load the skill at all).
|
|
3079
|
-
if (kind === 'skill' && (id === 'image-gen' || id === '
|
|
3080
|
-
const slugMap = { 'image-gen': 'infographic-generator', '
|
|
3007
|
+
if (kind === 'skill' && (id === 'image-gen' || id === 'learning-memory')) {
|
|
3008
|
+
const slugMap = { 'image-gen': 'infographic-generator', 'learning-memory': 'learning-memory' };
|
|
3081
3009
|
const slug = slugMap[id];
|
|
3082
3010
|
const rel = workspaceRelForAgent(agent, cfg, projectDir) || `workspace-${agent.id}`;
|
|
3083
3011
|
const folder = join(projectDir, '.openclaw', rel, 'skills', slug);
|
|
3084
3012
|
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
3085
3013
|
|
|
3086
|
-
// Sticker skill needs its outbound patch applied on enable / restored on disable (Zalo personal only).
|
|
3087
|
-
const applyStickerPatch = async (restore) => {
|
|
3088
|
-
if (id !== 'sticker-mention') return;
|
|
3089
|
-
const channel = (cfg.bindings || []).find((b) => b.agentId === agent.id)?.match?.channel || '';
|
|
3090
|
-
if (channel !== 'zalo-personal' && channel !== 'zalouser') return;
|
|
3091
|
-
let hostJs = join(projectDir, '.openclaw', rel, 'skills/zalo-sticker-mention/mentions.js');
|
|
3092
|
-
let contJs = `/home/node/project/.openclaw/${rel}/skills/zalo-sticker-mention/mentions.js`;
|
|
3093
|
-
if (!existsSync(hostJs)) {
|
|
3094
|
-
hostJs = join(projectDir, '.openclaw', rel, 'skills/sticker-mention/mentions.js');
|
|
3095
|
-
contJs = `/home/node/project/.openclaw/${rel}/skills/sticker-mention/mentions.js`;
|
|
3096
|
-
}
|
|
3097
|
-
if (!existsSync(hostJs)) return;
|
|
3098
|
-
const extra = restore ? ['--restore'] : [];
|
|
3099
|
-
try {
|
|
3100
|
-
if (hasDocker) {
|
|
3101
|
-
await runCapture('docker', ['exec', getBotContainerName(projectDir), 'node', contJs, ...extra], { cwd: projectDir, shell: false });
|
|
3102
|
-
} else {
|
|
3103
|
-
await run('node', [hostJs, ...extra], { cwd: projectDir });
|
|
3104
|
-
}
|
|
3105
|
-
} catch (e) { sendLog(`[zalo-patch] ${restore ? 'restore' : 'patch'} failed: ${e.message}`); }
|
|
3106
|
-
};
|
|
3107
|
-
|
|
3108
3014
|
let installedNow = false;
|
|
3109
3015
|
if (enabled) {
|
|
3110
3016
|
// Enable for THIS bot only: ensure its workspace has the skill folder.
|
|
3111
3017
|
if (!existsSync(folder)) { await installFeature(projectDir, agent.id, 'skill', id); installedNow = true; }
|
|
3112
|
-
await applyStickerPatch(false);
|
|
3113
3018
|
} else {
|
|
3114
|
-
// Disable for THIS bot only: restore any patch, then remove just this bot's folder.
|
|
3115
|
-
await applyStickerPatch(true);
|
|
3116
3019
|
await fsp.rm(folder, { recursive: true, force: true }).catch(() => {});
|
|
3117
3020
|
}
|
|
3118
3021
|
|
|
@@ -3267,7 +3170,6 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3267
3170
|
if (kind === 'skill') {
|
|
3268
3171
|
const skillSlugMap = {
|
|
3269
3172
|
'image-gen': 'infographic-generator',
|
|
3270
|
-
'sticker-mention': 'zalo-sticker-mention',
|
|
3271
3173
|
'learning-memory': 'learning-memory',
|
|
3272
3174
|
};
|
|
3273
3175
|
const slug = skillSlugMap[id] || id;
|
|
@@ -3614,7 +3516,6 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3614
3516
|
// THIS agent's workspace (not the global skills.entries flag, which would leak across bots).
|
|
3615
3517
|
const imageGenOn = isSkillFolderExists(projectDir, aid, 'infographic-generator', cfg);
|
|
3616
3518
|
const webSearchOn = isEnabled(['duckduckgo']);
|
|
3617
|
-
const stickerMentionOn = isSkillFolderExists(projectDir, aid, 'zalo-sticker-mention', cfg);
|
|
3618
3519
|
const learningMemoryOn = isSkillFolderExists(projectDir, aid, 'learning-memory', cfg);
|
|
3619
3520
|
const aliases = {
|
|
3620
3521
|
browser: ['openclaw-browser-automation', 'browser-automation'],
|
|
@@ -3629,7 +3530,6 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3629
3530
|
'skill:cron': cronOn,
|
|
3630
3531
|
'skill:image-gen': imageGenOn,
|
|
3631
3532
|
'skill:web-search': webSearchOn,
|
|
3632
|
-
'skill:sticker-mention': stickerMentionOn,
|
|
3633
3533
|
'skill:learning-memory': learningMemoryOn,
|
|
3634
3534
|
'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
|
|
3635
3535
|
'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
|
|
@@ -3645,7 +3545,6 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3645
3545
|
extensionDirExists(aliases) || isInstalledByRecord(aliases);
|
|
3646
3546
|
const installed = {
|
|
3647
3547
|
'skill:image-gen': isSkillFolderExists(projectDir, aid, 'infographic-generator', cfg),
|
|
3648
|
-
'skill:sticker-mention': isSkillFolderExists(projectDir, aid, 'zalo-sticker-mention', cfg),
|
|
3649
3548
|
'skill:learning-memory': isSkillFolderExists(projectDir, aid, 'learning-memory', cfg),
|
|
3650
3549
|
'plugin:openclaw-browser-automation': isActuallyInstalled(aliases.browser),
|
|
3651
3550
|
'plugin:openclaw-zalo-mod': isActuallyInstalled(aliases.zalo),
|
|
@@ -3659,7 +3558,6 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3659
3558
|
};
|
|
3660
3559
|
const versions = {
|
|
3661
3560
|
'skill:image-gen': await getInstalledSkillVersion(projectDir, aid, 'infographic-generator', cfg),
|
|
3662
|
-
'skill:sticker-mention': await getInstalledSkillVersion(projectDir, aid, 'zalo-sticker-mention', cfg),
|
|
3663
3561
|
'skill:learning-memory': await getInstalledSkillVersion(projectDir, aid, 'learning-memory', cfg),
|
|
3664
3562
|
'plugin:openclaw-browser-automation': await getInstalledPluginVersion(projectDir, aliases.browser),
|
|
3665
3563
|
'plugin:openclaw-zalo-mod': await getInstalledPluginVersion(projectDir, aliases.zalo),
|
|
@@ -3680,7 +3578,8 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3680
3578
|
fillVer('plugin:openclaw-fb-messenger', aliases.fbMessenger);
|
|
3681
3579
|
fillVer('plugin:memory-tencentdb', aliases.tencentMemory);
|
|
3682
3580
|
}
|
|
3683
|
-
|
|
3581
|
+
const zaloBackend = fresh.channels?.['zalo-connect']?.enabled ? 'zalo-connect' : '';
|
|
3582
|
+
return { flags, installed, versions, zaloBackend };
|
|
3684
3583
|
}
|
|
3685
3584
|
|
|
3686
3585
|
async function serveStatic(req, res) {
|
|
@@ -3895,6 +3794,14 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3895
3794
|
await saveState(rootProjectDir);
|
|
3896
3795
|
sendLog(`✅ Bot created: ${result.agentId} (${result.channel})`);
|
|
3897
3796
|
if (result.warning) sendLog(`⚠️ ${result.warning}`);
|
|
3797
|
+
// A first Zalo bot changes the project's docker infra needs (the entrypoint must
|
|
3798
|
+
// install the pinned zalo-connect plugin BEFORE the gateway starts). Force-resync so
|
|
3799
|
+
// the recreate below ships the zaloBackend-aware entrypoint — without this, the
|
|
3800
|
+
// login flow has to install mid-boot and restart the container, which can
|
|
3801
|
+
// interrupt OpenClaw's first-run migrations and wedge its state lease.
|
|
3802
|
+
if (result.channel === 'zalo-personal') {
|
|
3803
|
+
await syncDockerInfra(projectDir, true).catch((err) => sendLog(`[sync] infra resync failed: ${err.message}`));
|
|
3804
|
+
}
|
|
3898
3805
|
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
|
|
3899
3806
|
|
|
3900
3807
|
if (result.channel === 'telegram') {
|
|
@@ -3919,11 +3826,11 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3919
3826
|
// Delay login start to let the recreated container fully boot gateway + plugins
|
|
3920
3827
|
setTimeout(async () => {
|
|
3921
3828
|
try {
|
|
3922
|
-
const login = await
|
|
3923
|
-
if (login?.qrDataUrl) sendLog(`[
|
|
3924
|
-
if (login?.message) sendLog(`[
|
|
3829
|
+
const login = await startZaloLogin(projectDir, result.agentId);
|
|
3830
|
+
if (login?.qrDataUrl) sendLog(`[zalo-connect:qr] ${login.qrDataUrl}`);
|
|
3831
|
+
if (login?.message) sendLog(`[zalo-connect] ${login.message}`);
|
|
3925
3832
|
} catch (err) {
|
|
3926
|
-
sendLog(`[
|
|
3833
|
+
sendLog(`[zalo-connect] Login failed: ${err.message}`);
|
|
3927
3834
|
}
|
|
3928
3835
|
}, 5000);
|
|
3929
3836
|
}
|
|
@@ -3944,15 +3851,22 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3944
3851
|
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3945
3852
|
setImmediate(async () => {
|
|
3946
3853
|
try {
|
|
3947
|
-
const login = await
|
|
3948
|
-
if (login?.qrDataUrl) sendLog(`[
|
|
3949
|
-
if (login?.message) sendLog(`[
|
|
3854
|
+
const login = await startZaloLogin(projectDir, agentId);
|
|
3855
|
+
if (login?.qrDataUrl) sendLog(`[zalo-connect:qr] ${login.qrDataUrl}`);
|
|
3856
|
+
if (login?.message) sendLog(`[zalo-connect] ${login.message}`);
|
|
3950
3857
|
} catch (err) {
|
|
3951
|
-
sendLog(`[
|
|
3858
|
+
sendLog(`[zalo-connect] Login failed: ${err.message}`);
|
|
3952
3859
|
}
|
|
3953
3860
|
});
|
|
3954
3861
|
return json(res, { ok: true, message: 'Zalo login initiated. QR will appear in UI.' });
|
|
3955
3862
|
}
|
|
3863
|
+
if (url.pathname === '/api/zalo/login/cancel' && req.method === 'POST') {
|
|
3864
|
+
return json(res, cancelZaloLogin());
|
|
3865
|
+
}
|
|
3866
|
+
if (url.pathname === '/api/zalo/health' && req.method === 'GET') {
|
|
3867
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3868
|
+
return json(res, await getZaloHealth(projectDir));
|
|
3869
|
+
}
|
|
3956
3870
|
if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
|
|
3957
3871
|
const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
|
|
3958
3872
|
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
@@ -4201,7 +4115,4 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
|
|
|
4201
4115
|
printRemoteAccessHint(port).catch(() => {});
|
|
4202
4116
|
}
|
|
4203
4117
|
|
|
4204
|
-
export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig,
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4118
|
+
export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder };
|