create-openclaw-bot 5.8.4 → 5.8.8

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');
@@ -22,6 +22,27 @@ const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
23
  const WEB_DIR = resolve(__dirname, '../web');
24
24
  const SETUP_VERSION = (() => { try { return JSON.parse(fs.readFileSync(resolve(__dirname, '../../package.json'), 'utf8')).version || '0.0.0'; } catch { return '0.0.0'; } })();
25
+ let latestSetupVersionCache = SETUP_VERSION;
26
+ let isFetchingLatestSetup = false;
27
+
28
+ async function fetchLatestSetupVersionBg() {
29
+ if (isFetchingLatestSetup) return;
30
+ isFetchingLatestSetup = true;
31
+ try {
32
+ const resp = await fetch('https://registry.npmjs.org/create-openclaw-bot/latest', { signal: AbortSignal.timeout(4000) });
33
+ if (resp.ok) {
34
+ const data = await resp.json();
35
+ if (data.version) {
36
+ latestSetupVersionCache = data.version;
37
+ }
38
+ }
39
+ } catch (e) {
40
+ } finally {
41
+ isFetchingLatestSetup = false;
42
+ }
43
+ }
44
+ fetchLatestSetupVersionBg().catch(() => {});
45
+
25
46
  const DEFAULT_PROJECT_NAME = 'openclaw-bot';
26
47
  const STATE_FILE = '.openclaw-setup-state.json';
27
48
  const DEFAULT_MODEL = 'smart-route';
@@ -75,6 +96,31 @@ function detectOs() {
75
96
  return 'linux-desktop';
76
97
  }
77
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
+
78
124
  // Blacklist of Windows system/large directories that should never be walked
79
125
  const SYSTEM_DIR_BLACKLIST = new Set([
80
126
  'windows', 'program files', 'program files (x86)', 'programdata',
@@ -107,7 +153,7 @@ function recommendedMode(osChoice) {
107
153
  function commandExists(cmd, args = ['--version']) {
108
154
  return new Promise((resolve) => {
109
155
  const shell = process.platform === 'win32';
110
- execFile(cmd, args, { windowsHide: true, timeout: 5000, shell }, (err, stdout, stderr) => {
156
+ execFile(resolveBinPath(cmd), args, { windowsHide: true, timeout: 5000, shell }, (err, stdout, stderr) => {
111
157
  resolve({ ok: !err, output: String(stdout || stderr || '').trim() });
112
158
  });
113
159
  });
@@ -116,7 +162,7 @@ function commandExists(cmd, args = ['--version']) {
116
162
  function run(cmd, args, opts = {}) {
117
163
  return new Promise((resolve, reject) => {
118
164
  sendLog(`$ ${cmd} ${args.join(' ')}`);
119
- 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 || {}) } });
120
166
  let stdout = '';
121
167
  let resolved = false;
122
168
  child.stdout.on('data', (d) => {
@@ -146,7 +192,7 @@ function run(cmd, args, opts = {}) {
146
192
 
147
193
  function startDetached(cmd, args, opts = {}) {
148
194
  sendLog(`$ ${cmd} ${args.join(' ')} &`);
149
- const child = spawn(cmd, args, {
195
+ const child = spawn(resolveBinPath(cmd), args, {
150
196
  cwd: opts.cwd,
151
197
  shell: process.platform === 'win32',
152
198
  detached: true,
@@ -154,6 +200,10 @@ function startDetached(cmd, args, opts = {}) {
154
200
  windowsHide: opts.windowsHide ?? true,
155
201
  env: { ...process.env, ...(opts.env || {}) },
156
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
+ });
157
207
  child.unref();
158
208
  return child.pid;
159
209
  }
@@ -243,7 +293,7 @@ function runCapture(cmd, args, opts = {}) {
243
293
  return new Promise((resolve) => {
244
294
  let stdout = '';
245
295
  let stderr = '';
246
- const child = spawn(cmd, args, {
296
+ const child = spawn(resolveBinPath(cmd), args, {
247
297
  cwd: opts.cwd,
248
298
  shell: opts.shell ?? process.platform === 'win32',
249
299
  windowsHide: opts.windowsHide ?? true,
@@ -425,6 +475,8 @@ async function detectRuntime(projectDir) {
425
475
 
426
476
  async function syncRuntimeState(projectDir) {
427
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(() => {});
428
480
  await applyResolved9RouterApiKey(projectDir).catch(() => {});
429
481
  const rt = await detectRuntime(projectDir).catch(() => null);
430
482
  if (!rt) return;
@@ -444,6 +496,38 @@ async function syncRuntimeState(projectDir) {
444
496
  }
445
497
  }
446
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
+
447
531
  function uniqueSlug(base, used) {
448
532
  let out = base;
449
533
  let i = 2;
@@ -525,6 +609,10 @@ function ensureConfigShape(cfg) {
525
609
  delete agent.desc;
526
610
  delete agent.description;
527
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
+ }
528
616
  }
529
617
  agent.model = agent.model || { primary: cfg.agents.defaults.model.primary, fallbacks: [] };
530
618
  if (!agent.model.primary || agent.model.primary === '9router/smart-route' || agent.model.primary === 'openai/smart-route') agent.model.primary = DEFAULT_MODEL;
@@ -696,7 +784,7 @@ async function resolveProject9RouterApiKey(projectDir, cfg = null) {
696
784
  }
697
785
  const nativeApiKey = read9RouterApiKeyFromSqlite(join(projectDir || '', '.9router', 'db', 'data.sqlite'));
698
786
  if (nativeApiKey) return nativeApiKey;
699
- const homeApiKey = read9RouterApiKeyFromSqlite(join(os.homedir(), '.9router', 'db', 'data.sqlite'));
787
+ const homeApiKey = read9RouterApiKeyFromSqlite(join(getRealHomedir(), '.9router', 'db', 'data.sqlite'));
700
788
  if (homeApiKey) return homeApiKey;
701
789
  return '';
702
790
  }
@@ -954,7 +1042,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
954
1042
  providerKey: '9router',
955
1043
  deployMode: runtime.mode || state.mode || 'docker',
956
1044
  osChoice: runtime.os || state.os || detectOs(),
957
- selectedSkills: [],
1045
+ selectedSkills: ['memory', 'image-gen', 'web-search', 'scheduler'],
958
1046
  skills: dataExport.SKILLS || [],
959
1047
  agentMetas: [],
960
1048
  }));
@@ -974,7 +1062,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
974
1062
  cfg.agents.list.push({
975
1063
  id: agentId,
976
1064
  name: botName,
977
- workspace: `/root/project/.openclaw/${workspaceDir}`,
1065
+ workspace: `/home/node/project/.openclaw/${workspaceDir}`,
978
1066
  agentDir: `agents/${agentId}/agent`,
979
1067
  model: { primary: model === '9router/smart-route' || model === 'openai/smart-route' ? DEFAULT_MODEL : model, fallbacks: [] },
980
1068
  });
@@ -1022,7 +1110,9 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
1022
1110
  userInfo,
1023
1111
  agentWorkspaceDir: workspaceDir,
1024
1112
  workspacePath: `.openclaw/${workspaceDir}`,
1113
+ channel,
1025
1114
  hasZaloMod: channel === 'zalo-personal',
1115
+ hasZaloSticker: channel === 'zalo-personal',
1026
1116
  hasScheduler,
1027
1117
  hasImageGen,
1028
1118
  });
@@ -1053,7 +1143,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1053
1143
  const userDesc = String(body.userDescription || body.userDesc || '').trim();
1054
1144
  const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
1055
1145
  const workspaceDir = workspaceRelForAgent(agent, cfg, projectDir) || `workspace-${agentId}`;
1056
- agent.workspace = `/root/project/.openclaw/${workspaceDir}`;
1146
+ agent.workspace = `/home/node/project/.openclaw/${workspaceDir}`;
1057
1147
  agent.agentDir = `agents/${agentId}/agent`;
1058
1148
 
1059
1149
  // Find the existing accountId from bindings BEFORE removing them
@@ -1124,7 +1214,9 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1124
1214
  userInfo,
1125
1215
  agentWorkspaceDir: workspaceDir,
1126
1216
  workspacePath: `.openclaw/${workspaceDir}`,
1217
+ channel,
1127
1218
  hasZaloMod: channel === 'zalo-personal',
1219
+ hasZaloSticker: channel === 'zalo-personal',
1128
1220
  hasScheduler,
1129
1221
  hasImageGen,
1130
1222
  });
@@ -1133,6 +1225,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1133
1225
  await fsp.mkdir(dirname(join(wsRoot, name)), { recursive: true });
1134
1226
  await fsp.writeFile(join(wsRoot, name), content || '', 'utf8');
1135
1227
  }
1228
+
1136
1229
  return { ok: true, agentId, channel, workspace: `.openclaw/${workspaceDir}` };
1137
1230
  }
1138
1231
 
@@ -1196,8 +1289,46 @@ async function waitForGatewayZaloReady(botContainer, projectDir, timeoutMs = 900
1196
1289
  return ready;
1197
1290
  }
1198
1291
 
1199
- async function startZaloUserLogin(projectDir, mode = state.mode) {
1200
- 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
+
1201
1332
  if (zaloLoginInFlight) {
1202
1333
  setImmediate(async () => {
1203
1334
  try {
@@ -1214,7 +1345,7 @@ async function startZaloUserLogin(projectDir, mode = state.mode) {
1214
1345
  return { message: 'Zalo login is already running. Keep this modal open...' };
1215
1346
  }
1216
1347
  zaloLoginInFlight = true;
1217
- 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.`);
1218
1349
  const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
1219
1350
  if ((mode === 'docker' || existsSync(composeFile)) && existsSync(composeFile)) {
1220
1351
  const botContainer = getBotContainerName(projectDir);
@@ -1223,8 +1354,8 @@ async function startZaloUserLogin(projectDir, mode = state.mode) {
1223
1354
  const checkRegistryScript = `
1224
1355
  const fs = require('fs');
1225
1356
  try {
1226
- const dist = '/root/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist/index.js';
1227
- 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';
1228
1359
  if (!fs.existsSync(dist)) { console.log('MISSING'); process.exit(0); }
1229
1360
  if (!fs.existsSync(inst)) { console.log('MISSING_CHANNELS'); process.exit(0); }
1230
1361
  const j = JSON.parse(fs.readFileSync(inst, 'utf8'));
@@ -1244,17 +1375,17 @@ try {
1244
1375
  const fixScript = `
1245
1376
  const fs=require('fs');
1246
1377
  const cp=require('child_process');
1247
- const cfg='/root/project/.openclaw/openclaw.json';
1248
- 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';
1249
1380
  try{if(fs.existsSync(cfg))fs.copyFileSync(cfg,bk);}catch(e){}
1250
1381
  // Detect gateway version and pin zalouser plugin to match, preventing createSetupTranslator mismatch
1251
1382
  let gatewayVer='';
1252
1383
  try{gatewayVer=cp.execSync('openclaw --version 2>/dev/null',{encoding:'utf8'}).trim().replace(/[^0-9.]/g,'');}catch(e){}
1253
1384
  const pluginSpec=gatewayVer ? '@openclaw/zalouser@'+gatewayVer : '@openclaw/zalouser';
1254
1385
  console.log('Installing plugin via CLI: '+pluginSpec+'...');
1255
- 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){
1256
1387
  // Fallback: try without version pin if exact version not found on registry
1257
- 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');}}
1258
1389
  else{console.error('Install failed');}
1259
1390
  }
1260
1391
  try{
@@ -1278,10 +1409,10 @@ try{
1278
1409
  }catch(e){}
1279
1410
  try{
1280
1411
  console.log('Patching zalouser stability settings...');
1281
- 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'});
1282
1413
  }catch(e){}
1283
1414
  try{
1284
- const ep = '/root/project/docker/openclaw/entrypoint.sh';
1415
+ const ep = '/home/node/project/docker/openclaw/entrypoint.sh';
1285
1416
  if (fs.existsSync(ep)) {
1286
1417
  let content = fs.readFileSync(ep, 'utf8');
1287
1418
  if (!content.includes('zalo-monitor')) {
@@ -1312,11 +1443,12 @@ try{
1312
1443
  }
1313
1444
 
1314
1445
  // Clean old credentials & QR files inside container
1315
- const credPath = '/root/project/.openclaw/credentials/zalouser/credentials.json';
1316
- 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(() => {});
1317
1449
 
1318
1450
  sendLog('[zalouser] Generating Zalo QR. The image will appear automatically.');
1319
- 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`;
1320
1452
 
1321
1453
  // Retry-based login: the zalouser plugin may need time to connect to Zalo servers.
1322
1454
  // The CLI often exits with "Still preparing QR" on the first attempt.
@@ -1507,6 +1639,33 @@ async function recreateDockerBot(projectDir) {
1507
1639
  sendLog(`[docker] Recreating ${serviceName} to reload openclaw.json/.env...`);
1508
1640
  await run('docker', ['compose', '-f', composeFile, 'up', '-d', '--build', '--force-recreate', serviceName], { cwd: projectDir });
1509
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
+
1510
1669
  return true;
1511
1670
  }
1512
1671
 
@@ -1567,7 +1726,7 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
1567
1726
  await fsp.mkdir(openclawHome, { recursive: true });
1568
1727
  await fsp.mkdir(join(openclawHome, 'plugin-runtime-deps'), { recursive: true });
1569
1728
 
1570
- const selectedSkills = ['memory', 'image-gen', 'web-search'];
1729
+ const selectedSkills = ['memory', 'image-gen', 'web-search', 'scheduler'];
1571
1730
  const agentMetas = [];
1572
1731
  const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
1573
1732
  const cfg = buildOpenclawJson(common);
@@ -1631,7 +1790,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
1631
1790
  await fsp.mkdir(dockerDir, { recursive: true });
1632
1791
  const envContent = existsSync(rootEnvPath)
1633
1792
  ? await fsp.readFile(rootEnvPath, 'utf8')
1634
- : 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: '' });
1635
1794
  await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
1636
1795
  sendLog(`Docker env ready: ${dockerEnvPath}`);
1637
1796
  await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
@@ -1750,37 +1909,86 @@ async function loadSavedState(rootProjectDir) {
1750
1909
  }
1751
1910
  }
1752
1911
 
1912
+ function isRestrictedSystemDir(dirPath) {
1913
+ if (!dirPath) return true;
1914
+ const lower = resolve(dirPath).toLowerCase();
1915
+
1916
+ if (SYSTEM_DIR_BLACKLIST.has(basename(lower))) return true;
1917
+
1918
+ const winDir = process.env.SystemRoot ? resolve(process.env.SystemRoot).toLowerCase() : 'c:\\windows';
1919
+ const programFiles = process.env.ProgramFiles ? resolve(process.env.ProgramFiles).toLowerCase() : 'c:\\program files';
1920
+ const programFilesX86 = process.env['ProgramFiles(x86)'] ? resolve(process.env['ProgramFiles(x86)']).toLowerCase() : 'c:\\program files (x86)';
1921
+
1922
+ if (lower.startsWith(winDir) || lower.startsWith(programFiles) || lower.startsWith(programFilesX86)) {
1923
+ return true;
1924
+ }
1925
+
1926
+ if (lower.includes(':\\users\\') || lower.endsWith(':\\users')) {
1927
+ const home = resolve(getRealHomedir()).toLowerCase();
1928
+ if (lower !== home && !lower.startsWith(home + '\\') && !lower.startsWith(home + '/')) {
1929
+ return true;
1930
+ }
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
+
1947
+ return false;
1948
+ }
1949
+
1753
1950
  async function findLatestProject(rootProjectDir) {
1951
+ const realHome = getRealHomedir();
1754
1952
  const roots = [
1755
1953
  process.env.OPENCLAW_PROJECT_DIR,
1756
1954
  process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
1757
1955
  rootProjectDir,
1758
1956
  join(rootProjectDir, DEFAULT_PROJECT_NAME),
1759
1957
  dirname(rootProjectDir),
1760
- os.homedir(),
1761
- ];
1762
- // Scan all available drives, walking top-level dirs but skipping system folders
1958
+ realHome,
1959
+ join(realHome, 'Documents'),
1960
+ ].filter(Boolean);
1961
+
1763
1962
  const drives = await getAvailableDrives();
1764
1963
  for (const drive of drives) {
1765
1964
  const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
1766
1965
  for (const e of entries) {
1767
1966
  if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
1768
- roots.push(join(drive, e.name));
1967
+ const fullPath = join(drive, e.name);
1968
+ if (!isRestrictedSystemDir(fullPath)) {
1969
+ roots.push(fullPath);
1970
+ }
1769
1971
  }
1770
1972
  }
1771
1973
  }
1772
1974
  const candidates = [];
1975
+ const seen = new Set();
1773
1976
  async function walk(dir, depth = 0) {
1774
1977
  if (!dir || depth > 2 || !existsSync(dir)) return;
1775
- if (existsSync(join(dir, '.openclaw', 'openclaw.json'))) {
1776
- const st = await fsp.stat(join(dir, '.openclaw', 'openclaw.json')).catch(() => null);
1777
- if (st) candidates.push({ dir, mtimeMs: st.mtimeMs });
1978
+ const full = resolve(dir);
1979
+ if (isRestrictedSystemDir(full)) return;
1980
+ if (seen.has(full)) return;
1981
+ seen.add(full);
1982
+
1983
+ if (existsSync(join(full, '.openclaw', 'openclaw.json'))) {
1984
+ const st = await fsp.stat(join(full, '.openclaw', 'openclaw.json')).catch(() => null);
1985
+ if (st) candidates.push({ dir: full, mtimeMs: st.mtimeMs });
1778
1986
  return;
1779
1987
  }
1780
- const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
1988
+ const entries = await fsp.readdir(full, { withFileTypes: true }).catch(() => []);
1781
1989
  for (const e of entries) {
1782
1990
  if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
1783
- await walk(join(dir, e.name), depth + 1);
1991
+ await walk(join(full, e.name), depth + 1);
1784
1992
  }
1785
1993
  }
1786
1994
  }
@@ -1790,21 +1998,34 @@ async function findLatestProject(rootProjectDir) {
1790
1998
  }
1791
1999
 
1792
2000
  async function discoverProjects(rootProjectDir) {
2001
+ const realHome = getRealHomedir();
1793
2002
  const roots = [
1794
2003
  process.env.OPENCLAW_PROJECT_DIR,
1795
2004
  rootProjectDir,
1796
2005
  dirname(rootProjectDir),
1797
2006
  process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
1798
- ];
1799
- // Add all available drives for scanning
2007
+ realHome,
2008
+ join(realHome, 'Documents'),
2009
+ ].filter(Boolean);
2010
+
1800
2011
  const drives = await getAvailableDrives();
1801
- for (const drive of drives) roots.push(drive);
2012
+ for (const drive of drives) {
2013
+ const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
2014
+ for (const e of entries) {
2015
+ if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
2016
+ const fullPath = join(drive, e.name);
2017
+ if (!isRestrictedSystemDir(fullPath)) {
2018
+ roots.push(fullPath);
2019
+ }
2020
+ }
2021
+ }
2022
+ }
1802
2023
  const seen = new Set();
1803
2024
  const hits = [];
1804
2025
  async function walk(dir, depth = 0) {
1805
2026
  if (!dir || depth > 2 || !existsSync(dir)) return;
1806
2027
  const full = resolve(dir);
1807
- if (full === resolve(os.homedir())) return;
2028
+ if (isRestrictedSystemDir(full)) return;
1808
2029
  if (seen.has(full)) return;
1809
2030
  seen.add(full);
1810
2031
  const cfgPath = join(full, '.openclaw', 'openclaw.json');
@@ -1832,7 +2053,7 @@ async function discoverProjects(rootProjectDir) {
1832
2053
  const entries = await fsp.readdir(full, { withFileTypes: true }).catch(() => []);
1833
2054
  for (const e of entries) {
1834
2055
  if (!e.isDirectory()) continue;
1835
- if (e.name === 'node_modules' || e.name.startsWith('.git') || SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) continue;
2056
+ if (e.name === 'node_modules' || e.name.startsWith('.') || SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) continue;
1836
2057
  await walk(join(full, e.name), depth + 1);
1837
2058
  }
1838
2059
  }
@@ -1907,9 +2128,10 @@ async function connectPickedProject(projectName, rootProjectDir) {
1907
2128
 
1908
2129
  async function deleteProjectFolder(projectDir, rootProjectDir) {
1909
2130
  const resolved = resolve(String(projectDir || ''));
1910
- const home = resolve(os.homedir());
2131
+ const home = resolve(getRealHomedir());
2132
+ const rootHome = resolve(os.homedir());
1911
2133
  if (!existsSync(join(resolved, '.openclaw', 'openclaw.json'))) throw httpError(404, 'openclaw.json not found in selected project');
1912
- 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');
1913
2135
  const projects = await discoverProjects(rootProjectDir).catch(() => []);
1914
2136
  const meta = projects.find((p) => resolve(p.projectDir) === resolved);
1915
2137
  if (!meta || !meta.botCount) throw httpError(403, 'Refusing to delete a folder that is not a detected bot project');
@@ -2020,6 +2242,12 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2020
2242
  }
2021
2243
 
2022
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
+
2023
2251
  if (enabled) {
2024
2252
  cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
2025
2253
  cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:automation']));
@@ -2103,6 +2331,51 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2103
2331
  }
2104
2332
  }
2105
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
+
2106
2379
  if (kind === 'plugin') {
2107
2380
  cfg.plugins = cfg.plugins || { entries: {} };
2108
2381
  cfg.plugins.entries = cfg.plugins.entries || {};
@@ -2160,7 +2433,7 @@ async function installFeature(projectDir, agentId, kind, id) {
2160
2433
  const botContainer = getBotContainerName(projectDir);
2161
2434
  sendLog(`[plugin] Installing/updating clawhub:${id} inside container ${botContainer}...`);
2162
2435
 
2163
- const cmd = `cd /root/project && openclaw plugins install clawhub:${id} --force`;
2436
+ const cmd = `cd /home/node/project && openclaw plugins install clawhub:${id} --force`;
2164
2437
  const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
2165
2438
 
2166
2439
  if (cmdOut) {
@@ -2316,7 +2589,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
2316
2589
  const cfg = existsSync(cfgPath) ? ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'))) : {};
2317
2590
  const aid = agentId || cfg.agents?.list?.[0]?.id || 'bot';
2318
2591
  const browserOn = !!cfg.browser?.enabled;
2319
- 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');
2320
2593
  const fresh = cfg;
2321
2594
  const freshSaved = {};
2322
2595
  const installsPath = join(projectDir || '', '.openclaw', 'plugins', 'installs.json');
@@ -2345,6 +2618,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
2345
2618
  );
2346
2619
  const imageGenOn = !!cfg.skills?.entries?.['image-gen']?.enabled;
2347
2620
  const webSearchOn = isEnabled(['duckduckgo']);
2621
+ const stickerMentionOn = !!cfg.skills?.entries?.['sticker-mention']?.enabled;
2348
2622
  const aliases = {
2349
2623
  browser: ['openclaw-browser-automation', 'browser-automation'],
2350
2624
  zalo: ['openclaw-zalo-mod', 'zalo-mod'],
@@ -2356,6 +2630,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
2356
2630
  'skill:cron': cronOn,
2357
2631
  'skill:image-gen': imageGenOn,
2358
2632
  'skill:web-search': webSearchOn,
2633
+ 'skill:sticker-mention': stickerMentionOn,
2359
2634
  'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
2360
2635
  'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
2361
2636
  'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
@@ -2425,14 +2700,8 @@ async function handler(req, res, rootProjectDir) {
2425
2700
  };
2426
2701
  const projects = await discoverProjects(rootProjectDir).catch(() => []);
2427
2702
 
2428
- let latestSetupVersion = SETUP_VERSION;
2429
- try {
2430
- const resp = await fetch('https://registry.npmjs.org/create-openclaw-bot/latest', { signal: AbortSignal.timeout(3000) });
2431
- if (resp.ok) {
2432
- const data = await resp.json();
2433
- if (data.version) latestSetupVersion = data.version;
2434
- }
2435
- } catch (e) {}
2703
+ fetchLatestSetupVersionBg().catch(() => {});
2704
+ const latestSetupVersion = latestSetupVersionCache;
2436
2705
 
2437
2706
  return json(res, {
2438
2707
  os: osChoice,
@@ -2581,7 +2850,7 @@ async function handler(req, res, rootProjectDir) {
2581
2850
  const token = String(body.token || '').trim();
2582
2851
  sendLog(`[telegram] Registering Telegram channel via CLI inside ${botContainer}...`);
2583
2852
  try {
2584
- 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 });
2585
2854
  sendLog(`[telegram] CLI registration output:\n${regResult.stdout}\n${regResult.stderr}`);
2586
2855
  sendLog(`[telegram] Restarting ${botContainer} container to load the registered channel...`);
2587
2856
  await restartDockerBotContainer(projectDir).catch((err) => sendLog(`[telegram] Container restart failed: ${err.message}`));
@@ -2598,7 +2867,7 @@ async function handler(req, res, rootProjectDir) {
2598
2867
  // Delay login start to let the recreated container fully boot gateway + plugins
2599
2868
  setTimeout(async () => {
2600
2869
  try {
2601
- const login = await startZaloUserLogin(projectDir, state.mode);
2870
+ const login = await startZaloUserLogin(projectDir, state.mode, result.agentId);
2602
2871
  if (login?.qrDataUrl) sendLog(`[zalouser:qr] ${login.qrDataUrl}`);
2603
2872
  if (login?.message) sendLog(`[zalouser] ${login.message}`);
2604
2873
  } catch (err) {
@@ -2618,10 +2887,12 @@ async function handler(req, res, rootProjectDir) {
2618
2887
  return json(res, result);
2619
2888
  }
2620
2889
  if (url.pathname === '/api/zalo/login' && req.method === 'POST') {
2621
- const projectDir = await resolveProjectDir(rootProjectDir);
2890
+ const body = await readJson(req).catch(() => ({}));
2891
+ const agentId = body.agentId || '';
2892
+ const projectDir = await resolveProjectDir(rootProjectDir, body);
2622
2893
  setImmediate(async () => {
2623
2894
  try {
2624
- const login = await startZaloUserLogin(projectDir, state.mode);
2895
+ const login = await startZaloUserLogin(projectDir, state.mode, agentId);
2625
2896
  if (login?.qrDataUrl) sendLog(`[zalouser:qr] ${login.qrDataUrl}`);
2626
2897
  if (login?.message) sendLog(`[zalouser] ${login.message}`);
2627
2898
  } catch (err) {
@@ -2704,6 +2975,9 @@ function openUrl(url) {
2704
2975
  const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
2705
2976
  const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
2706
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
+ });
2707
2981
  child.unref();
2708
2982
  }
2709
2983