create-openclaw-bot 5.8.5 → 5.8.9

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.
@@ -13,7 +13,7 @@ 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 } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
16
+ const { buildWorkspaceFileMap, buildCronjobSkillMd, buildInfographicGeneratorSkillMd, buildInfographicGeneratorJs, buildStickerMentionSkillMd, buildStickerMentionJs } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
17
17
  const { buildOpenclawJson, buildEnvFileContent, buildExecApprovalsJson } = loadSharedModule('../setup/shared/bot-config-gen.js', '__openclawBotConfig');
18
18
  const { buildDockerArtifacts } = loadSharedModule('../setup/shared/docker-gen.js', '__openclawDockerGen');
19
19
  const { OPENCLAW_NPM_SPEC, NINE_ROUTER_NPM_SPEC, build9RouterProviderConfig, get9RouterBaseUrl } = loadSharedModule('../setup/shared/common-gen.js', '__openclawCommon');
@@ -96,6 +96,31 @@ function detectOs() {
96
96
  return 'linux-desktop';
97
97
  }
98
98
 
99
+ function getRealHomedir() {
100
+ const home = os.homedir();
101
+ if (process.platform === 'win32') return home;
102
+ const sudoUser = process.env.SUDO_USER;
103
+ if (sudoUser && (home === '/root' || home.startsWith('/root/'))) {
104
+ const userHome = process.platform === 'darwin' ? `/Users/${sudoUser}` : `/home/${sudoUser}`;
105
+ if (existsSync(userHome)) {
106
+ return userHome;
107
+ }
108
+ }
109
+ return home;
110
+ }
111
+
112
+ function resolveBinPath(cmd) {
113
+ if (!cmd || cmd.includes('/') || cmd.includes('\\')) return cmd;
114
+ const nodeBinDir = dirname(process.argv[0]);
115
+ const localPath = join(nodeBinDir, process.platform === 'win32' ? `${cmd}.cmd` : cmd);
116
+ if (existsSync(localPath)) return localPath;
117
+ const localExe = join(nodeBinDir, process.platform === 'win32' ? `${cmd}.exe` : cmd);
118
+ if (existsSync(localExe)) return localExe;
119
+ const nodeModulesBin = join(process.cwd(), 'node_modules', '.bin', process.platform === 'win32' ? `${cmd}.cmd` : cmd);
120
+ if (existsSync(nodeModulesBin)) return nodeModulesBin;
121
+ return cmd;
122
+ }
123
+
99
124
  // Blacklist of Windows system/large directories that should never be walked
100
125
  const SYSTEM_DIR_BLACKLIST = new Set([
101
126
  'windows', 'program files', 'program files (x86)', 'programdata',
@@ -128,7 +153,7 @@ function recommendedMode(osChoice) {
128
153
  function commandExists(cmd, args = ['--version']) {
129
154
  return new Promise((resolve) => {
130
155
  const shell = process.platform === 'win32';
131
- execFile(cmd, args, { windowsHide: true, timeout: 5000, shell }, (err, stdout, stderr) => {
156
+ execFile(resolveBinPath(cmd), args, { windowsHide: true, timeout: 5000, shell }, (err, stdout, stderr) => {
132
157
  resolve({ ok: !err, output: String(stdout || stderr || '').trim() });
133
158
  });
134
159
  });
@@ -137,7 +162,7 @@ function commandExists(cmd, args = ['--version']) {
137
162
  function run(cmd, args, opts = {}) {
138
163
  return new Promise((resolve, reject) => {
139
164
  sendLog(`$ ${cmd} ${args.join(' ')}`);
140
- const child = spawn(cmd, args, { cwd: opts.cwd, shell: process.platform === 'win32', env: { ...process.env, ...(opts.env || {}) } });
165
+ const child = spawn(resolveBinPath(cmd), args, { cwd: opts.cwd, shell: process.platform === 'win32', env: { ...process.env, ...(opts.env || {}) } });
141
166
  let stdout = '';
142
167
  let resolved = false;
143
168
  child.stdout.on('data', (d) => {
@@ -167,7 +192,7 @@ function run(cmd, args, opts = {}) {
167
192
 
168
193
  function startDetached(cmd, args, opts = {}) {
169
194
  sendLog(`$ ${cmd} ${args.join(' ')} &`);
170
- const child = spawn(cmd, args, {
195
+ const child = spawn(resolveBinPath(cmd), args, {
171
196
  cwd: opts.cwd,
172
197
  shell: process.platform === 'win32',
173
198
  detached: true,
@@ -175,6 +200,10 @@ function startDetached(cmd, args, opts = {}) {
175
200
  windowsHide: opts.windowsHide ?? true,
176
201
  env: { ...process.env, ...(opts.env || {}) },
177
202
  });
203
+ child.on('error', (err) => {
204
+ sendLog(`[error] Failed to start background command "${cmd}": ${err.message}`);
205
+ console.error(`Failed to start background command "${cmd}":`, err);
206
+ });
178
207
  child.unref();
179
208
  return child.pid;
180
209
  }
@@ -264,7 +293,7 @@ function runCapture(cmd, args, opts = {}) {
264
293
  return new Promise((resolve) => {
265
294
  let stdout = '';
266
295
  let stderr = '';
267
- const child = spawn(cmd, args, {
296
+ const child = spawn(resolveBinPath(cmd), args, {
268
297
  cwd: opts.cwd,
269
298
  shell: opts.shell ?? process.platform === 'win32',
270
299
  windowsHide: opts.windowsHide ?? true,
@@ -446,6 +475,8 @@ async function detectRuntime(projectDir) {
446
475
 
447
476
  async function syncRuntimeState(projectDir) {
448
477
  if (!projectDir || !existsSync(join(projectDir, '.openclaw', 'openclaw.json'))) return;
478
+ // Auto-migrate legacy /root/project paths → /home/node/project in openclaw.json
479
+ await migrateContainerPaths(projectDir).catch(() => {});
449
480
  await applyResolved9RouterApiKey(projectDir).catch(() => {});
450
481
  const rt = await detectRuntime(projectDir).catch(() => null);
451
482
  if (!rt) return;
@@ -465,6 +496,38 @@ async function syncRuntimeState(projectDir) {
465
496
  }
466
497
  }
467
498
 
499
+ /**
500
+ * Migrate legacy /root/project/ paths to /home/node/project/ in openclaw.json.
501
+ * Old projects may have been created with /root/project/ which doesn't match the
502
+ * Docker volume mount point (/home/node/project/.openclaw).
503
+ * Also clears stale workspace attestation files to prevent WorkspaceVanishedError.
504
+ */
505
+ async function migrateContainerPaths(projectDir) {
506
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
507
+ if (!existsSync(cfgPath)) return;
508
+ let raw = await fsp.readFile(cfgPath, 'utf8');
509
+ if (!raw.includes('/root/project/')) return;
510
+ // Replace all /root/project/ references with /home/node/project/
511
+ const updated = raw.replace(/\/root\/project\//g, '/home/node/project/');
512
+ if (updated !== raw) {
513
+ await fsp.writeFile(cfgPath, updated, 'utf8');
514
+ sendLog('[migrate] Fixed legacy /root/project/ paths → /home/node/project/ in openclaw.json');
515
+ // Clear stale workspace attestations to avoid WorkspaceVanishedError
516
+ const attestDir = join(projectDir, '.openclaw', 'workspace-attestations');
517
+ if (existsSync(attestDir)) {
518
+ try {
519
+ const files = await fsp.readdir(attestDir);
520
+ for (const f of files) {
521
+ if (f.endsWith('.attested')) {
522
+ await fsp.unlink(join(attestDir, f)).catch(() => {});
523
+ }
524
+ }
525
+ sendLog('[migrate] Cleared stale workspace attestation files');
526
+ } catch {}
527
+ }
528
+ }
529
+ }
530
+
468
531
  function uniqueSlug(base, used) {
469
532
  let out = base;
470
533
  let i = 2;
@@ -546,6 +609,10 @@ function ensureConfigShape(cfg) {
546
609
  delete agent.desc;
547
610
  delete agent.description;
548
611
  delete agent.persona;
612
+ // Auto-fix legacy /root/project paths → /home/node/project (Docker container path)
613
+ if (agent.workspace && agent.workspace.includes('/root/project/')) {
614
+ agent.workspace = agent.workspace.replace('/root/project/', '/home/node/project/');
615
+ }
549
616
  }
550
617
  agent.model = agent.model || { primary: cfg.agents.defaults.model.primary, fallbacks: [] };
551
618
  if (!agent.model.primary || agent.model.primary === '9router/smart-route' || agent.model.primary === 'openai/smart-route') agent.model.primary = DEFAULT_MODEL;
@@ -717,7 +784,7 @@ async function resolveProject9RouterApiKey(projectDir, cfg = null) {
717
784
  }
718
785
  const nativeApiKey = read9RouterApiKeyFromSqlite(join(projectDir || '', '.9router', 'db', 'data.sqlite'));
719
786
  if (nativeApiKey) return nativeApiKey;
720
- const homeApiKey = read9RouterApiKeyFromSqlite(join(os.homedir(), '.9router', 'db', 'data.sqlite'));
787
+ const homeApiKey = read9RouterApiKeyFromSqlite(join(getRealHomedir(), '.9router', 'db', 'data.sqlite'));
721
788
  if (homeApiKey) return homeApiKey;
722
789
  return '';
723
790
  }
@@ -975,7 +1042,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
975
1042
  providerKey: '9router',
976
1043
  deployMode: runtime.mode || state.mode || 'docker',
977
1044
  osChoice: runtime.os || state.os || detectOs(),
978
- selectedSkills: [],
1045
+ selectedSkills: ['memory', 'image-gen', 'web-search', 'scheduler'],
979
1046
  skills: dataExport.SKILLS || [],
980
1047
  agentMetas: [],
981
1048
  }));
@@ -995,7 +1062,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
995
1062
  cfg.agents.list.push({
996
1063
  id: agentId,
997
1064
  name: botName,
998
- workspace: `/root/project/.openclaw/${workspaceDir}`,
1065
+ workspace: `/home/node/project/.openclaw/${workspaceDir}`,
999
1066
  agentDir: `agents/${agentId}/agent`,
1000
1067
  model: { primary: model === '9router/smart-route' || model === 'openai/smart-route' ? DEFAULT_MODEL : model, fallbacks: [] },
1001
1068
  });
@@ -1043,7 +1110,9 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
1043
1110
  userInfo,
1044
1111
  agentWorkspaceDir: workspaceDir,
1045
1112
  workspacePath: `.openclaw/${workspaceDir}`,
1113
+ channel,
1046
1114
  hasZaloMod: channel === 'zalo-personal',
1115
+ hasZaloSticker: channel === 'zalo-personal',
1047
1116
  hasScheduler,
1048
1117
  hasImageGen,
1049
1118
  });
@@ -1074,7 +1143,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1074
1143
  const userDesc = String(body.userDescription || body.userDesc || '').trim();
1075
1144
  const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
1076
1145
  const workspaceDir = workspaceRelForAgent(agent, cfg, projectDir) || `workspace-${agentId}`;
1077
- agent.workspace = `/root/project/.openclaw/${workspaceDir}`;
1146
+ agent.workspace = `/home/node/project/.openclaw/${workspaceDir}`;
1078
1147
  agent.agentDir = `agents/${agentId}/agent`;
1079
1148
 
1080
1149
  // Find the existing accountId from bindings BEFORE removing them
@@ -1145,7 +1214,9 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1145
1214
  userInfo,
1146
1215
  agentWorkspaceDir: workspaceDir,
1147
1216
  workspacePath: `.openclaw/${workspaceDir}`,
1217
+ channel,
1148
1218
  hasZaloMod: channel === 'zalo-personal',
1219
+ hasZaloSticker: channel === 'zalo-personal',
1149
1220
  hasScheduler,
1150
1221
  hasImageGen,
1151
1222
  });
@@ -1154,6 +1225,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1154
1225
  await fsp.mkdir(dirname(join(wsRoot, name)), { recursive: true });
1155
1226
  await fsp.writeFile(join(wsRoot, name), content || '', 'utf8');
1156
1227
  }
1228
+
1157
1229
  return { ok: true, agentId, channel, workspace: `.openclaw/${workspaceDir}` };
1158
1230
  }
1159
1231
 
@@ -1217,8 +1289,46 @@ async function waitForGatewayZaloReady(botContainer, projectDir, timeoutMs = 900
1217
1289
  return ready;
1218
1290
  }
1219
1291
 
1220
- async function startZaloUserLogin(projectDir, mode = state.mode) {
1221
- const qrPaths = ['/tmp/openclaw/openclaw-zalouser-qr.png', '/tmp/openclaw/openclaw-zalouser-qr-default.png'];
1292
+ async function startZaloUserLogin(projectDir, mode = state.mode, agentId = '') {
1293
+ let profile = 'default';
1294
+ if (agentId) {
1295
+ try {
1296
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
1297
+ if (existsSync(cfgPath)) {
1298
+ const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
1299
+ const binding = (cfg.bindings || []).find(b => b.agentId === agentId && b.match?.channel === 'zalouser');
1300
+ if (binding?.match?.accountId) {
1301
+ profile = binding.match.accountId;
1302
+ }
1303
+ }
1304
+ } catch (e) {
1305
+ sendLog(`[zalouser] Warning: Failed to parse openclaw.json: ${e.message}`);
1306
+ }
1307
+ } else if (state.activeBotId) {
1308
+ try {
1309
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
1310
+ if (existsSync(cfgPath)) {
1311
+ const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
1312
+ const binding = (cfg.bindings || []).find(b => b.agentId === state.activeBotId && b.match?.channel === 'zalouser');
1313
+ if (binding?.match?.accountId) {
1314
+ profile = binding.match.accountId;
1315
+ }
1316
+ }
1317
+ } catch (e) {}
1318
+ }
1319
+
1320
+ const qrPaths = [
1321
+ `/tmp/openclaw/openclaw-zalouser-qr-${profile}.png`,
1322
+ `/tmp/openclaw-1000/openclaw-zalouser-qr-${profile}.png`
1323
+ ];
1324
+ if (profile === 'default') {
1325
+ qrPaths.push(
1326
+ '/tmp/openclaw/openclaw-zalouser-qr.png',
1327
+ '/tmp/openclaw-1000/openclaw-zalouser-qr.png',
1328
+ '/tmp/openclaw/openclaw-zalouser-qr-default.png'
1329
+ );
1330
+ }
1331
+
1222
1332
  if (zaloLoginInFlight) {
1223
1333
  setImmediate(async () => {
1224
1334
  try {
@@ -1235,7 +1345,7 @@ async function startZaloUserLogin(projectDir, mode = state.mode) {
1235
1345
  return { message: 'Zalo login is already running. Keep this modal open...' };
1236
1346
  }
1237
1347
  zaloLoginInFlight = true;
1238
- sendLog('[zalouser] Preparing login. QR will be generated for the UI modal.');
1348
+ sendLog(`[zalouser] Preparing login for profile [${profile}]. QR will be generated for the UI modal.`);
1239
1349
  const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
1240
1350
  if ((mode === 'docker' || existsSync(composeFile)) && existsSync(composeFile)) {
1241
1351
  const botContainer = getBotContainerName(projectDir);
@@ -1244,8 +1354,8 @@ async function startZaloUserLogin(projectDir, mode = state.mode) {
1244
1354
  const checkRegistryScript = `
1245
1355
  const fs = require('fs');
1246
1356
  try {
1247
- const dist = '/root/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist/index.js';
1248
- const inst = '/root/project/.openclaw/plugins/installs.json';
1357
+ const dist = '/home/node/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist/index.js';
1358
+ const inst = '/home/node/project/.openclaw/plugins/installs.json';
1249
1359
  if (!fs.existsSync(dist)) { console.log('MISSING'); process.exit(0); }
1250
1360
  if (!fs.existsSync(inst)) { console.log('MISSING_CHANNELS'); process.exit(0); }
1251
1361
  const j = JSON.parse(fs.readFileSync(inst, 'utf8'));
@@ -1265,17 +1375,17 @@ try {
1265
1375
  const fixScript = `
1266
1376
  const fs=require('fs');
1267
1377
  const cp=require('child_process');
1268
- const cfg='/root/project/.openclaw/openclaw.json';
1269
- const bk='/root/project/.openclaw/openclaw.json.zalo-backup';
1378
+ const cfg='/home/node/project/.openclaw/openclaw.json';
1379
+ const bk='/home/node/project/.openclaw/openclaw.json.zalo-backup';
1270
1380
  try{if(fs.existsSync(cfg))fs.copyFileSync(cfg,bk);}catch(e){}
1271
1381
  // Detect gateway version and pin zalouser plugin to match, preventing createSetupTranslator mismatch
1272
1382
  let gatewayVer='';
1273
1383
  try{gatewayVer=cp.execSync('openclaw --version 2>/dev/null',{encoding:'utf8'}).trim().replace(/[^0-9.]/g,'');}catch(e){}
1274
1384
  const pluginSpec=gatewayVer ? '@openclaw/zalouser@'+gatewayVer : '@openclaw/zalouser';
1275
1385
  console.log('Installing plugin via CLI: '+pluginSpec+'...');
1276
- try{cp.execSync('cd /root/project && openclaw plugins install '+pluginSpec+' --force',{stdio:'inherit'});}catch(e){
1386
+ try{cp.execSync('cd /home/node/project && openclaw plugins install '+pluginSpec+' --force',{stdio:'inherit'});}catch(e){
1277
1387
  // Fallback: try without version pin if exact version not found on registry
1278
- if(gatewayVer){console.log('Pinned version failed, trying latest...');try{cp.execSync('cd /root/project && openclaw plugins install @openclaw/zalouser --force',{stdio:'inherit'});}catch(e2){console.error('Install failed');}}
1388
+ if(gatewayVer){console.log('Pinned version failed, trying latest...');try{cp.execSync('cd /home/node/project && openclaw plugins install @openclaw/zalouser --force',{stdio:'inherit'});}catch(e2){console.error('Install failed');}}
1279
1389
  else{console.error('Install failed');}
1280
1390
  }
1281
1391
  try{
@@ -1299,10 +1409,10 @@ try{
1299
1409
  }catch(e){}
1300
1410
  try{
1301
1411
  console.log('Patching zalouser stability settings...');
1302
- cp.execSync('ZALO_JS=$(find "/root/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'});
1412
+ 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'});
1303
1413
  }catch(e){}
1304
1414
  try{
1305
- const ep = '/root/project/docker/openclaw/entrypoint.sh';
1415
+ const ep = '/home/node/project/docker/openclaw/entrypoint.sh';
1306
1416
  if (fs.existsSync(ep)) {
1307
1417
  let content = fs.readFileSync(ep, 'utf8');
1308
1418
  if (!content.includes('zalo-monitor')) {
@@ -1333,11 +1443,12 @@ try{
1333
1443
  }
1334
1444
 
1335
1445
  // Clean old credentials & QR files inside container
1336
- const credPath = '/root/project/.openclaw/credentials/zalouser/credentials.json';
1337
- await runCapture('docker', ['exec', botContainer, 'sh', '-lc', `rm -f ${credPath} ${qrPaths.join(' ')}`], { cwd: projectDir, shell: false });
1446
+ const credFile = profile === 'default' ? 'credentials.json' : `credentials-${profile}.json`;
1447
+ const credPath = `/home/node/project/.openclaw/credentials/zalouser/${credFile}`;
1448
+ await runCapture('docker', ['exec', botContainer, 'sh', '-lc', `rm -f ${credPath} ${qrPaths.join(' ')}`], { cwd: projectDir, shell: false }).catch(() => {});
1338
1449
 
1339
1450
  sendLog('[zalouser] Generating Zalo QR. The image will appear automatically.');
1340
- const loginCmd = 'cd /root/project && openclaw channels login --channel zalouser --verbose';
1451
+ const loginCmd = `cd /home/node/project && openclaw channels login --channel zalouser --account ${profile} --verbose`;
1341
1452
 
1342
1453
  // Retry-based login: the zalouser plugin may need time to connect to Zalo servers.
1343
1454
  // The CLI often exits with "Still preparing QR" on the first attempt.
@@ -1528,6 +1639,33 @@ async function recreateDockerBot(projectDir) {
1528
1639
  sendLog(`[docker] Recreating ${serviceName} to reload openclaw.json/.env...`);
1529
1640
  await run('docker', ['compose', '-f', composeFile, 'up', '-d', '--build', '--force-recreate', serviceName], { cwd: projectDir });
1530
1641
  await waitForDockerContainer(containerName);
1642
+
1643
+ // Automatically run Zalo sticker-mention patch if skill is enabled and agent is zalo-personal
1644
+ try {
1645
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
1646
+ if (existsSync(cfgPath)) {
1647
+ const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
1648
+ const stickerMentionOn = !!cfg.skills?.entries?.['sticker-mention']?.enabled;
1649
+ if (stickerMentionOn) {
1650
+ for (const a of cfg.agents?.list || []) {
1651
+ const binding = (cfg.bindings || []).find((b) => b.agentId === a.id);
1652
+ const channel = binding?.match?.channel || 'telegram';
1653
+ if (channel === 'zalo-personal') {
1654
+ const workspaceDir = workspaceRelForAgent(a, cfg, projectDir) || `workspace-${a.id}`;
1655
+ const mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/sticker-mention/mentions.js');
1656
+ if (existsSync(mentionsJsPath)) {
1657
+ sendLog(`[zalo-patch] Automatically running mentions.js inside container ${containerName}...`);
1658
+ const patchCmd = await runCapture('docker', ['exec', containerName, 'node', `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`], { cwd: projectDir, shell: false });
1659
+ sendLog(`[zalo-patch] Output: ${patchCmd.stdout || ''} ${patchCmd.stderr || ''}`);
1660
+ }
1661
+ }
1662
+ }
1663
+ }
1664
+ }
1665
+ } catch (err) {
1666
+ sendLog(`[zalo-patch] Failed to auto-run mentions.js: ${err.message}`);
1667
+ }
1668
+
1531
1669
  return true;
1532
1670
  }
1533
1671
 
@@ -1588,7 +1726,7 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
1588
1726
  await fsp.mkdir(openclawHome, { recursive: true });
1589
1727
  await fsp.mkdir(join(openclawHome, 'plugin-runtime-deps'), { recursive: true });
1590
1728
 
1591
- const selectedSkills = ['memory', 'image-gen', 'web-search'];
1729
+ const selectedSkills = ['memory', 'image-gen', 'web-search', 'scheduler'];
1592
1730
  const agentMetas = [];
1593
1731
  const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
1594
1732
  const cfg = buildOpenclawJson(common);
@@ -1652,7 +1790,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
1652
1790
  await fsp.mkdir(dockerDir, { recursive: true });
1653
1791
  const envContent = existsSync(rootEnvPath)
1654
1792
  ? await fsp.readFile(rootEnvPath, 'utf8')
1655
- : buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: [], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
1793
+ : buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'image-gen', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
1656
1794
  await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
1657
1795
  sendLog(`Docker env ready: ${dockerEnvPath}`);
1658
1796
  await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
@@ -1786,23 +1924,39 @@ function isRestrictedSystemDir(dirPath) {
1786
1924
  }
1787
1925
 
1788
1926
  if (lower.includes(':\\users\\') || lower.endsWith(':\\users')) {
1789
- const home = resolve(os.homedir()).toLowerCase();
1927
+ const home = resolve(getRealHomedir()).toLowerCase();
1790
1928
  if (lower !== home && !lower.startsWith(home + '\\') && !lower.startsWith(home + '/')) {
1791
1929
  return true;
1792
1930
  }
1793
1931
  }
1932
+
1933
+ if (process.platform !== 'win32') {
1934
+ const unixBlacklist = new Set([
1935
+ 'usr', 'var', 'proc', 'sys', 'dev', 'etc', 'sbin', 'bin', 'lib', 'lib64', 'run', 'tmp', 'boot', 'lost+found', 'srv', 'mnt', 'media', 'opt'
1936
+ ]);
1937
+ if (unixBlacklist.has(basename(lower))) return true;
1938
+
1939
+ if (lower.startsWith('/home') || lower.startsWith('/root')) {
1940
+ const realHome = resolve(getRealHomedir()).toLowerCase();
1941
+ if (lower !== realHome && !lower.startsWith(realHome + '/')) {
1942
+ return true;
1943
+ }
1944
+ }
1945
+ }
1946
+
1794
1947
  return false;
1795
1948
  }
1796
1949
 
1797
1950
  async function findLatestProject(rootProjectDir) {
1951
+ const realHome = getRealHomedir();
1798
1952
  const roots = [
1799
1953
  process.env.OPENCLAW_PROJECT_DIR,
1800
1954
  process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
1801
1955
  rootProjectDir,
1802
1956
  join(rootProjectDir, DEFAULT_PROJECT_NAME),
1803
1957
  dirname(rootProjectDir),
1804
- os.homedir(),
1805
- join(os.homedir(), 'Documents'),
1958
+ realHome,
1959
+ join(realHome, 'Documents'),
1806
1960
  ].filter(Boolean);
1807
1961
 
1808
1962
  const drives = await getAvailableDrives();
@@ -1844,13 +1998,14 @@ async function findLatestProject(rootProjectDir) {
1844
1998
  }
1845
1999
 
1846
2000
  async function discoverProjects(rootProjectDir) {
2001
+ const realHome = getRealHomedir();
1847
2002
  const roots = [
1848
2003
  process.env.OPENCLAW_PROJECT_DIR,
1849
2004
  rootProjectDir,
1850
2005
  dirname(rootProjectDir),
1851
2006
  process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
1852
- os.homedir(),
1853
- join(os.homedir(), 'Documents'),
2007
+ realHome,
2008
+ join(realHome, 'Documents'),
1854
2009
  ].filter(Boolean);
1855
2010
 
1856
2011
  const drives = await getAvailableDrives();
@@ -1973,9 +2128,10 @@ async function connectPickedProject(projectName, rootProjectDir) {
1973
2128
 
1974
2129
  async function deleteProjectFolder(projectDir, rootProjectDir) {
1975
2130
  const resolved = resolve(String(projectDir || ''));
1976
- const home = resolve(os.homedir());
2131
+ const home = resolve(getRealHomedir());
2132
+ const rootHome = resolve(os.homedir());
1977
2133
  if (!existsSync(join(resolved, '.openclaw', 'openclaw.json'))) throw httpError(404, 'openclaw.json not found in selected project');
1978
- if (resolved === home || /^[A-Za-z]:\\?$/.test(resolved)) throw httpError(403, 'Refusing to delete home/root folder');
2134
+ if (resolved === home || resolved === rootHome || /^[A-Za-z]:\\?$/.test(resolved)) throw httpError(403, 'Refusing to delete home/root folder');
1979
2135
  const projects = await discoverProjects(rootProjectDir).catch(() => []);
1980
2136
  const meta = projects.find((p) => resolve(p.projectDir) === resolved);
1981
2137
  if (!meta || !meta.botCount) throw httpError(403, 'Refusing to delete a folder that is not a detected bot project');
@@ -2086,6 +2242,12 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2086
2242
  }
2087
2243
 
2088
2244
  if (kind === 'skill' && id === 'cron') {
2245
+ cfg.skills = cfg.skills || { entries: {} };
2246
+ cfg.skills.entries = cfg.skills.entries || {};
2247
+ delete cfg.skills.entries['cron'];
2248
+ cfg.skills.entries['cronjob'] = cfg.skills.entries['cronjob'] || {};
2249
+ cfg.skills.entries['cronjob'].enabled = !!enabled;
2250
+
2089
2251
  if (enabled) {
2090
2252
  cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
2091
2253
  cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:automation']));
@@ -2169,6 +2331,51 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2169
2331
  }
2170
2332
  }
2171
2333
 
2334
+ if (kind === 'skill' && id === 'sticker-mention') {
2335
+ cfg.skills = cfg.skills || { entries: {} };
2336
+ cfg.skills.entries = cfg.skills.entries || {};
2337
+ cfg.skills.entries['sticker-mention'] = cfg.skills.entries['sticker-mention'] || {};
2338
+ cfg.skills.entries['sticker-mention'].enabled = !!enabled;
2339
+
2340
+ for (const a of cfg.agents.list) {
2341
+ const sf = await readWorkspaceText(projectDir, a, 'skills/sticker-mention/SKILL.md');
2342
+ const jsf = await readWorkspaceText(projectDir, a, 'skills/sticker-mention/mentions.js');
2343
+ if (enabled) {
2344
+ await fsp.mkdir(dirname(sf.file), { recursive: true });
2345
+ await fsp.writeFile(sf.file, buildStickerMentionSkillMd(), 'utf8');
2346
+ await fsp.writeFile(jsf.file, buildStickerMentionJs(), 'utf8');
2347
+ } else {
2348
+ const binding = (cfg.bindings || []).find((b) => b.agentId === a.id);
2349
+ const channel = binding?.match?.channel || 'telegram';
2350
+ if (channel === 'zalo-personal' && existsSync(jsf.file)) {
2351
+ try {
2352
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2353
+ if (hasDocker) {
2354
+ const botContainer = getBotContainerName(projectDir);
2355
+ const workspaceDir = workspaceRelForAgent(a, cfg, projectDir) || `workspace-${a.id}`;
2356
+ sendLog('[zalo-patch] Running restore before disabling skill...');
2357
+ await runCapture('docker', ['exec', botContainer, 'node', `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`, '--restore'], { cwd: projectDir, shell: false });
2358
+ }
2359
+ } catch (e) {
2360
+ sendLog(`[zalo-patch] Restore failed: ${e.message}`);
2361
+ }
2362
+ }
2363
+ if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
2364
+ if (existsSync(jsf.file)) await fsp.rm(jsf.file, { force: true });
2365
+ }
2366
+ }
2367
+
2368
+ // Write cfgPath early so recreation reads updated openclaw.json
2369
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2370
+
2371
+ // Recreate container to apply updated openclaw.json
2372
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2373
+ if (hasDocker) {
2374
+ sendLog(`[docker] Sticker & Mention skill toggled to ${enabled}. Recreating containers...`);
2375
+ await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
2376
+ }
2377
+ }
2378
+
2172
2379
  if (kind === 'plugin') {
2173
2380
  cfg.plugins = cfg.plugins || { entries: {} };
2174
2381
  cfg.plugins.entries = cfg.plugins.entries || {};
@@ -2226,7 +2433,7 @@ async function installFeature(projectDir, agentId, kind, id) {
2226
2433
  const botContainer = getBotContainerName(projectDir);
2227
2434
  sendLog(`[plugin] Installing/updating clawhub:${id} inside container ${botContainer}...`);
2228
2435
 
2229
- const cmd = `cd /root/project && openclaw plugins install clawhub:${id} --force`;
2436
+ const cmd = `cd /home/node/project && openclaw plugins install clawhub:${id} --force`;
2230
2437
  const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
2231
2438
 
2232
2439
  if (cmdOut) {
@@ -2382,7 +2589,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
2382
2589
  const cfg = existsSync(cfgPath) ? ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'))) : {};
2383
2590
  const aid = agentId || cfg.agents?.list?.[0]?.id || 'bot';
2384
2591
  const browserOn = !!cfg.browser?.enabled;
2385
- const cronOn = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
2592
+ const cronOn = !!cfg.skills?.entries?.['cronjob']?.enabled || !!cfg.skills?.entries?.['cron']?.enabled || !!(cfg.tools?.alsoAllow || []).includes('group:automation');
2386
2593
  const fresh = cfg;
2387
2594
  const freshSaved = {};
2388
2595
  const installsPath = join(projectDir || '', '.openclaw', 'plugins', 'installs.json');
@@ -2411,6 +2618,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
2411
2618
  );
2412
2619
  const imageGenOn = !!cfg.skills?.entries?.['image-gen']?.enabled;
2413
2620
  const webSearchOn = isEnabled(['duckduckgo']);
2621
+ const stickerMentionOn = !!cfg.skills?.entries?.['sticker-mention']?.enabled;
2414
2622
  const aliases = {
2415
2623
  browser: ['openclaw-browser-automation', 'browser-automation'],
2416
2624
  zalo: ['openclaw-zalo-mod', 'zalo-mod'],
@@ -2422,6 +2630,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
2422
2630
  'skill:cron': cronOn,
2423
2631
  'skill:image-gen': imageGenOn,
2424
2632
  'skill:web-search': webSearchOn,
2633
+ 'skill:sticker-mention': stickerMentionOn,
2425
2634
  'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
2426
2635
  'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
2427
2636
  'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
@@ -2641,7 +2850,7 @@ async function handler(req, res, rootProjectDir) {
2641
2850
  const token = String(body.token || '').trim();
2642
2851
  sendLog(`[telegram] Registering Telegram channel via CLI inside ${botContainer}...`);
2643
2852
  try {
2644
- const regResult = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', `cd /root/project && openclaw channels add telegram --token "${token}"`], { cwd: projectDir, shell: false });
2853
+ const regResult = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', `cd /home/node/project && openclaw channels add telegram --token "${token}"`], { cwd: projectDir, shell: false });
2645
2854
  sendLog(`[telegram] CLI registration output:\n${regResult.stdout}\n${regResult.stderr}`);
2646
2855
  sendLog(`[telegram] Restarting ${botContainer} container to load the registered channel...`);
2647
2856
  await restartDockerBotContainer(projectDir).catch((err) => sendLog(`[telegram] Container restart failed: ${err.message}`));
@@ -2658,7 +2867,7 @@ async function handler(req, res, rootProjectDir) {
2658
2867
  // Delay login start to let the recreated container fully boot gateway + plugins
2659
2868
  setTimeout(async () => {
2660
2869
  try {
2661
- const login = await startZaloUserLogin(projectDir, state.mode);
2870
+ const login = await startZaloUserLogin(projectDir, state.mode, result.agentId);
2662
2871
  if (login?.qrDataUrl) sendLog(`[zalouser:qr] ${login.qrDataUrl}`);
2663
2872
  if (login?.message) sendLog(`[zalouser] ${login.message}`);
2664
2873
  } catch (err) {
@@ -2678,10 +2887,12 @@ async function handler(req, res, rootProjectDir) {
2678
2887
  return json(res, result);
2679
2888
  }
2680
2889
  if (url.pathname === '/api/zalo/login' && req.method === 'POST') {
2681
- const projectDir = await resolveProjectDir(rootProjectDir);
2890
+ const body = await readJson(req).catch(() => ({}));
2891
+ const agentId = body.agentId || '';
2892
+ const projectDir = await resolveProjectDir(rootProjectDir, body);
2682
2893
  setImmediate(async () => {
2683
2894
  try {
2684
- const login = await startZaloUserLogin(projectDir, state.mode);
2895
+ const login = await startZaloUserLogin(projectDir, state.mode, agentId);
2685
2896
  if (login?.qrDataUrl) sendLog(`[zalouser:qr] ${login.qrDataUrl}`);
2686
2897
  if (login?.message) sendLog(`[zalouser] ${login.message}`);
2687
2898
  } catch (err) {
@@ -2764,6 +2975,9 @@ function openUrl(url) {
2764
2975
  const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
2765
2976
  const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
2766
2977
  const child = spawn(cmd, args, { detached: true, stdio: 'ignore', shell: false, windowsHide: true });
2978
+ child.on('error', (err) => {
2979
+ sendLog(`[openUrl] Warning: Could not open browser automatically (${err.message}). Please navigate to ${url} manually.`);
2980
+ });
2767
2981
  child.unref();
2768
2982
  }
2769
2983
 
@@ -30,7 +30,7 @@
30
30
  // Keep in sync with setup/data/skills.js skill IDs
31
31
  const SKILLS = [
32
32
  { value: 'memory', name: '🧠 Long-term Memory (⭐ Khuyên dùng)', slug: 'memory' },
33
- { value: 'scheduler', name: '⏰ Native Cron Scheduler (⭐ Khuyên dùng)', slug: null },
33
+ { value: 'scheduler', name: '⏰ Native Cron Scheduler (⭐ Khuyên dùng)', slug: 'cronjob' },
34
34
  { value: 'rag', name: '📚 RAG / Knowledge Base', slug: 'rag' },
35
35
  { value: 'image-gen', name: '🎨 Image Generation (DALL·E / Flux)', slug: 'image-gen' },
36
36
  { value: 'code-interpreter', name: '💻 Code Interpreter (Python/JS)', slug: 'code-interpreter' },
@@ -85,7 +85,7 @@
85
85
  const agentsList = agentMetas.map((meta) => ({
86
86
  id: meta.agentId,
87
87
  ...(meta.name ? { name: meta.name } : {}),
88
- workspace: `/root/project/.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
88
+ workspace: `/home/node/project/.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
89
89
  agentDir: `agents/${meta.agentId}/agent`,
90
90
  model: { primary: model, fallbacks: [] },
91
91
  }));
@@ -261,6 +261,7 @@ If setup reported a plugin install error, run this after the bot is running:
261
261
  name: 'Smart Proxy (Auto Route)',
262
262
  contextWindow: 200000,
263
263
  maxTokens: 8192,
264
+ input: ['text', 'image'],
264
265
  },
265
266
  ],
266
267
  };