create-openclaw-bot 5.8.16 → 5.8.18

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  # 🦞 OpenClaw Setup
4
4
 
5
5
  <p align="center">
6
- <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.8.16-0EA5E9?style=for-the-badge" alt="Version 5.8.16" /></a>
6
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.8.17-0EA5E9?style=for-the-badge" alt="Version 5.8.17" /></a>
7
7
  <a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
8
8
  <a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
9
9
  <a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
@@ -23,7 +23,7 @@ A next-generation **Web UI Setup** and management dashboard that automates 100%
23
23
 
24
24
  ---
25
25
 
26
- ## 🆕 What's New in v5.8.16
26
+ ## 🆕 What's New in v5.8.17
27
27
 
28
28
  ### 🚀 New Features: Deep Integration of Infographic Image Generator, Zalo Sticker & Auto-Tag Skills, and Workspace Docs Optimization
29
29
 
@@ -173,6 +173,6 @@ Yes. You can edit the config JSON directly via the integrated File Editor in the
173
173
 
174
174
  <div align="center">
175
175
 
176
- Made with 🦞 by [Kent](https://github.com/tuanminhhole)
176
+ Made with 🦞 by [tuanminhhole](https://github.com/tuanminhhole)
177
177
 
178
178
  </div>
package/README.vi.md CHANGED
@@ -3,7 +3,7 @@
3
3
  # 🦞 OpenClaw Setup
4
4
 
5
5
  <p align="center">
6
- <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.8.16-0EA5E9?style=for-the-badge" alt="Version 5.8.16" /></a>
6
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.8.17-0EA5E9?style=for-the-badge" alt="Version 5.8.17" /></a>
7
7
  <a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
8
8
  <a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
9
9
  <a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
@@ -23,7 +23,7 @@ Trình cài đặt và quản trị **Web UI Setup** thế hệ mới giúp tự
23
23
 
24
24
  ---
25
25
 
26
- ## 🆕 Có gì mới trong v5.8.16
26
+ ## 🆕 Có gì mới trong v5.8.17
27
27
 
28
28
  ### 🚀 Tính năng mới: Tích hợp sâu Skill Tạo ảnh Infographic, Skill Sticker & Auto-Tag (Zalo) cùng Tối ưu hóa Workspace Docs
29
29
 
@@ -88,6 +88,7 @@ const state = {
88
88
  mode: null,
89
89
  os: null,
90
90
  startedAt: null,
91
+ projects: null,
91
92
  };
92
93
 
93
94
  function sendLog(line) {
@@ -1074,7 +1075,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
1074
1075
  const emoji = String(body.emoji || '').trim();
1075
1076
  const userName = String(body.userName || '').trim();
1076
1077
  const userDesc = String(body.userDescription || body.userDesc || '').trim();
1077
- const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
1078
+ const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- ** tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
1078
1079
 
1079
1080
  const openclawHome = join(projectDir, '.openclaw');
1080
1081
  await fsp.mkdir(openclawHome, { recursive: true });
@@ -1085,7 +1086,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
1085
1086
  providerKey: '9router',
1086
1087
  deployMode: runtime.mode || state.mode || 'docker',
1087
1088
  osChoice: runtime.os || state.os || detectOs(),
1088
- selectedSkills: ['memory', 'image-gen', 'web-search', 'scheduler'],
1089
+ selectedSkills: ['memory', 'web-search', 'scheduler'],
1089
1090
  skills: dataExport.SKILLS || [],
1090
1091
  agentMetas: [],
1091
1092
  }));
@@ -1156,7 +1157,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
1156
1157
  workspacePath: `/home/node/project/.openclaw/${workspaceDir}`,
1157
1158
  channel,
1158
1159
  hasZaloMod: channel === 'zalo-personal',
1159
- hasZaloSticker: channel === 'zalo-personal',
1160
+ hasZaloSticker: false,
1160
1161
  hasScheduler,
1161
1162
  hasImageGen,
1162
1163
  });
@@ -1261,7 +1262,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1261
1262
  workspacePath: `/home/node/project/.openclaw/${workspaceDir}`,
1262
1263
  channel,
1263
1264
  hasZaloMod: channel === 'zalo-personal',
1264
- hasZaloSticker: channel === 'zalo-personal',
1265
+ hasZaloSticker: false,
1265
1266
  hasScheduler,
1266
1267
  hasImageGen,
1267
1268
  });
@@ -1697,10 +1698,15 @@ async function recreateDockerBot(projectDir) {
1697
1698
  const channel = binding?.match?.channel || 'telegram';
1698
1699
  if (channel === 'zalo-personal' || channel === 'zalouser') {
1699
1700
  const workspaceDir = workspaceRelForAgent(a, cfg, projectDir) || `workspace-${a.id}`;
1700
- const mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/sticker-mention/mentions.js');
1701
+ let mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/zalo-sticker-mention/mentions.js');
1702
+ let containerJsPath = `/home/node/project/.openclaw/${workspaceDir}/skills/zalo-sticker-mention/mentions.js`;
1703
+ if (!existsSync(mentionsJsPath)) {
1704
+ mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/sticker-mention/mentions.js');
1705
+ containerJsPath = `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`;
1706
+ }
1701
1707
  if (existsSync(mentionsJsPath)) {
1702
1708
  sendLog(`[zalo-patch] Automatically running mentions.js inside container ${containerName}...`);
1703
- const patchCmd = await runCapture('docker', ['exec', containerName, 'node', `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`], { cwd: projectDir, shell: false });
1709
+ const patchCmd = await runCapture('docker', ['exec', containerName, 'node', containerJsPath], { cwd: projectDir, shell: false });
1704
1710
  sendLog(`[zalo-patch] Output: ${patchCmd.stdout || ''} ${patchCmd.stderr || ''}`);
1705
1711
  }
1706
1712
  }
@@ -1771,7 +1777,7 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
1771
1777
  await fsp.mkdir(openclawHome, { recursive: true });
1772
1778
  await fsp.mkdir(join(openclawHome, 'plugin-runtime-deps'), { recursive: true });
1773
1779
 
1774
- const selectedSkills = ['memory', 'image-gen', 'web-search', 'scheduler'];
1780
+ const selectedSkills = ['memory', 'web-search', 'scheduler'];
1775
1781
  const agentMetas = [];
1776
1782
  const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
1777
1783
  const cfg = buildOpenclawJson(common);
@@ -1833,7 +1839,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
1833
1839
  await fsp.mkdir(dockerDir, { recursive: true });
1834
1840
  const envContent = existsSync(rootEnvPath)
1835
1841
  ? await fsp.readFile(rootEnvPath, 'utf8')
1836
- : buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'image-gen', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
1842
+ : buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: ['memory', 'web-search', 'scheduler'], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
1837
1843
  await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
1838
1844
  sendLog(`Docker env ready: ${dockerEnvPath}`);
1839
1845
  await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
@@ -1939,6 +1945,7 @@ async function saveState(rootProjectDir) {
1939
1945
  gatewayPort: state.gatewayPort,
1940
1946
  routerUrl: state.routerUrl,
1941
1947
  routerPort: state.routerPort,
1948
+ projects: state.projects || [],
1942
1949
  }, null, 2), 'utf8').catch(() => {});
1943
1950
  }
1944
1951
 
@@ -1946,6 +1953,9 @@ async function loadSavedState(rootProjectDir) {
1946
1953
  const file = join(rootProjectDir, STATE_FILE);
1947
1954
  if (!existsSync(file)) return;
1948
1955
  const saved = JSON.parse(await fsp.readFile(file, 'utf8'));
1956
+ if (Array.isArray(saved?.projects)) {
1957
+ state.projects = saved.projects;
1958
+ }
1949
1959
  if (saved?.projectDir && existsSync(join(saved.projectDir, '.openclaw', 'openclaw.json'))) {
1950
1960
  Object.assign(state, saved, { installed: !!saved.installed });
1951
1961
  await syncRuntimeState(state.projectDir);
@@ -1969,20 +1979,31 @@ function isRestrictedSystemDir(dirPath) {
1969
1979
  if (lower.includes(':\\users\\') || lower.endsWith(':\\users')) {
1970
1980
  const home = resolve(getRealHomedir()).toLowerCase();
1971
1981
  if (lower !== home && !lower.startsWith(home + '\\') && !lower.startsWith(home + '/')) {
1972
- return true;
1982
+ const cwd = resolve(process.cwd()).toLowerCase();
1983
+ const match = cwd.match(/^([a-z]:\\users\\[^\\]+)/) || cwd.match(/^(\/mnt\/[a-z]\/users\/[^\/]+)/);
1984
+ const cwdHome = match ? match[1] : '';
1985
+ if (!cwdHome || (lower !== cwdHome && !lower.startsWith(cwdHome + '\\') && !lower.startsWith(cwdHome + '/'))) {
1986
+ return true;
1987
+ }
1973
1988
  }
1974
1989
  }
1975
1990
 
1976
1991
  if (process.platform !== 'win32') {
1977
1992
  const unixBlacklist = new Set([
1978
- 'usr', 'var', 'proc', 'sys', 'dev', 'etc', 'sbin', 'bin', 'lib', 'lib64', 'run', 'tmp', 'boot', 'lost+found', 'srv', 'mnt', 'media', 'opt'
1993
+ 'usr', 'var', 'proc', 'sys', 'dev', 'etc', 'sbin', 'bin', 'lib', 'lib64', 'run', 'tmp', 'boot', 'lost+found', 'srv', 'mnt', 'media', 'opt',
1994
+ 'applications', 'library', 'system', 'volumes', 'private', 'cores', 'network', 'users'
1979
1995
  ]);
1980
1996
  if (unixBlacklist.has(basename(lower))) return true;
1981
1997
 
1982
- if (lower.startsWith('/home') || lower.startsWith('/root')) {
1998
+ if (lower.startsWith('/home') || lower.startsWith('/root') || lower.startsWith('/mnt/') || lower.startsWith('/users/') || lower === '/users') {
1983
1999
  const realHome = resolve(getRealHomedir()).toLowerCase();
1984
2000
  if (lower !== realHome && !lower.startsWith(realHome + '/')) {
1985
- return true;
2001
+ const cwd = resolve(process.cwd()).toLowerCase();
2002
+ const match = cwd.match(/^(\/home\/[^\/]+)/) || cwd.match(/^(\/root)/) || cwd.match(/^(\/mnt\/[a-z]\/users\/[^\/]+)/) || cwd.match(/^(\/users\/[^\/]+)/);
2003
+ const cwdHome = match ? match[1] : '';
2004
+ if (!cwdHome || (lower !== cwdHome && !lower.startsWith(cwdHome + '/'))) {
2005
+ return true;
2006
+ }
1986
2007
  }
1987
2008
  }
1988
2009
  }
@@ -2040,73 +2061,52 @@ async function findLatestProject(rootProjectDir) {
2040
2061
  return candidates[0]?.dir || null;
2041
2062
  }
2042
2063
 
2043
- async function discoverProjects(rootProjectDir) {
2044
- const realHome = getRealHomedir();
2045
- const roots = [
2046
- process.env.OPENCLAW_PROJECT_DIR,
2047
- rootProjectDir,
2048
- dirname(rootProjectDir),
2049
- process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
2050
- realHome,
2051
- join(realHome, 'Documents'),
2052
- ].filter(Boolean);
2053
-
2054
- const drives = await getAvailableDrives();
2055
- for (const drive of drives) {
2056
- const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
2057
- for (const e of entries) {
2058
- if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
2059
- const fullPath = join(drive, e.name);
2060
- if (!isRestrictedSystemDir(fullPath)) {
2061
- roots.push(fullPath);
2062
- }
2064
+ async function ensureProjectsLoaded(rootProjectDir) {
2065
+ if (state.projects !== null) return;
2066
+ state.projects = [];
2067
+ const file = join(rootProjectDir, STATE_FILE);
2068
+ if (existsSync(file)) {
2069
+ try {
2070
+ const saved = JSON.parse(await fsp.readFile(file, 'utf8'));
2071
+ if (Array.isArray(saved?.projects)) {
2072
+ state.projects = saved.projects;
2063
2073
  }
2064
- }
2074
+ } catch {}
2065
2075
  }
2066
- const seen = new Set();
2067
- const hits = [];
2068
- async function walk(dir, depth = 0) {
2069
- if (!dir || depth > 2 || !existsSync(dir)) return;
2070
- const full = resolve(dir);
2071
- if (isRestrictedSystemDir(full)) return;
2072
- if (seen.has(full)) return;
2073
- seen.add(full);
2074
- const cfgPath = join(full, '.openclaw', 'openclaw.json');
2075
- if (existsSync(cfgPath)) {
2076
- const st = await fsp.stat(cfgPath).catch(() => null);
2077
- const runtime = await detectRuntime(full).catch(() => ({ mode: 'unknown', gatewayPort: 0, routerPort: 0, syncSource: 'config' }));
2078
- const bots = await listConfiguredBots(full).catch(() => []);
2079
- const uniqueBotCount = new Set(bots.map((b) => b.id)).size;
2080
- const hasDocker = existsSync(join(full, 'docker', 'openclaw', 'docker-compose.yml'));
2081
- const isLikelyProject = uniqueBotCount > 0 || hasDocker || existsSync(join(full, '.env')) || existsSync(join(full, 'package.json'));
2082
- if (!isLikelyProject) return;
2083
- hits.push({
2084
- projectDir: full,
2085
- os: process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux',
2086
- mode: runtime.mode || 'unknown',
2087
- gatewayPort: runtime.gatewayPort || 0,
2088
- routerPort: runtime.routerPort || 0,
2089
- syncSource: runtime.syncSource || 'config',
2090
- botCount: uniqueBotCount,
2091
- hasDocker,
2092
- updatedAt: st?.mtimeMs || 0,
2093
- });
2094
- return;
2076
+ }
2077
+
2078
+ async function discoverProjects(rootProjectDir) {
2079
+ await ensureProjectsLoaded(rootProjectDir);
2080
+
2081
+ if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
2082
+ const resolved = resolve(state.projectDir);
2083
+ if (!state.projects.some(p => resolve(p.projectDir) === resolved)) {
2084
+ const meta = await buildProjectMeta(resolved).catch(() => null);
2085
+ if (meta) state.projects.push(meta);
2095
2086
  }
2096
- const entries = await fsp.readdir(full, { withFileTypes: true }).catch(() => []);
2097
- for (const e of entries) {
2098
- if (!e.isDirectory()) continue;
2099
- if (e.name === 'node_modules' || e.name.startsWith('.') || SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) continue;
2100
- await walk(join(full, e.name), depth + 1);
2087
+ }
2088
+
2089
+ const updatedProjects = [];
2090
+ for (const p of state.projects) {
2091
+ if (existsSync(join(p.projectDir, '.openclaw', 'openclaw.json'))) {
2092
+ const meta = await buildProjectMeta(p.projectDir).catch(() => null);
2093
+ if (meta) {
2094
+ updatedProjects.push(meta);
2095
+ }
2101
2096
  }
2102
2097
  }
2103
- for (const root of roots) await walk(root);
2104
- hits.sort((a, b) =>
2105
- (b.botCount - a.botCount) ||
2106
- (Number(b.hasDocker) - Number(a.hasDocker)) ||
2107
- (b.updatedAt - a.updatedAt)
2108
- );
2109
- return hits.slice(0, 20);
2098
+ state.projects = updatedProjects;
2099
+
2100
+ state.projects.sort((a, b) => {
2101
+ const aActive = state.projectDir && resolve(state.projectDir) === resolve(a.projectDir);
2102
+ const bActive = state.projectDir && resolve(state.projectDir) === resolve(b.projectDir);
2103
+ if (aActive && !bActive) return -1;
2104
+ if (!aActive && bActive) return 1;
2105
+ return (b.botCount - a.botCount) || (b.updatedAt - a.updatedAt);
2106
+ });
2107
+
2108
+ await saveState(rootProjectDir).catch(() => {});
2109
+ return state.projects.slice(0, 20);
2110
2110
  }
2111
2111
 
2112
2112
  async function resolveProjectDir(rootProjectDir, body = {}) {
@@ -2138,10 +2138,37 @@ async function resolveProjectDir(rootProjectDir, body = {}) {
2138
2138
  return state.projectDir;
2139
2139
  }
2140
2140
 
2141
+ async function buildProjectMeta(projectDir) {
2142
+ const full = resolve(projectDir);
2143
+ const cfgPath = join(full, '.openclaw', 'openclaw.json');
2144
+ const st = await fsp.stat(cfgPath).catch(() => null);
2145
+ const runtime = await detectRuntime(full).catch(() => ({ mode: 'unknown', gatewayPort: 0, routerPort: 0, syncSource: 'config' }));
2146
+ const bots = await listConfiguredBots(full).catch(() => []);
2147
+ const uniqueBotCount = new Set(bots.map((b) => b.id)).size;
2148
+ const hasDocker = existsSync(join(full, 'docker', 'openclaw', 'docker-compose.yml'));
2149
+ return {
2150
+ projectDir: full,
2151
+ os: process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux',
2152
+ mode: runtime.mode || 'unknown',
2153
+ gatewayPort: runtime.gatewayPort || 0,
2154
+ routerPort: runtime.routerPort || 0,
2155
+ syncSource: runtime.syncSource || 'config',
2156
+ botCount: uniqueBotCount,
2157
+ hasDocker,
2158
+ updatedAt: st?.mtimeMs || 0,
2159
+ };
2160
+ }
2161
+
2141
2162
  async function connectExistingProject(projectDir, rootProjectDir) {
2142
2163
  const resolved = resolve(String(projectDir || ''));
2143
2164
  if (!existsSync(join(resolved, '.openclaw', 'openclaw.json'))) throw httpError(404, 'openclaw.json not found in selected project');
2144
2165
  await syncRuntimeState(resolved);
2166
+ await ensureProjectsLoaded(rootProjectDir);
2167
+ const meta = await buildProjectMeta(resolved).catch(() => null);
2168
+ if (meta) {
2169
+ state.projects = state.projects.filter(p => resolve(p.projectDir) !== resolved);
2170
+ state.projects.unshift(meta);
2171
+ }
2145
2172
  await saveState(rootProjectDir);
2146
2173
  const bots = await listConfiguredBots(resolved).catch(() => []);
2147
2174
  return {
@@ -2195,6 +2222,8 @@ async function deleteProjectFolder(projectDir, rootProjectDir) {
2195
2222
  state.projectDir = null;
2196
2223
  state.installed = false;
2197
2224
  }
2225
+ await ensureProjectsLoaded(rootProjectDir);
2226
+ state.projects = state.projects.filter(p => resolve(p.projectDir) !== resolved);
2198
2227
  await saveState(rootProjectDir);
2199
2228
  return { ok: true, projectDir: resolved };
2200
2229
  }
@@ -2324,19 +2353,6 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2324
2353
  cfg.skills.entries['image-gen'] = cfg.skills.entries['image-gen'] || {};
2325
2354
  cfg.skills.entries['image-gen'].enabled = !!enabled;
2326
2355
 
2327
- for (const a of cfg.agents.list) {
2328
- const sf = await readWorkspaceText(projectDir, a, 'skills/infographic-generator/SKILL.md');
2329
- const js = await readWorkspaceText(projectDir, a, 'skills/infographic-generator/image-generator.js');
2330
- if (enabled) {
2331
- await fsp.mkdir(dirname(sf.file), { recursive: true });
2332
- await fsp.writeFile(sf.file, buildInfographicGeneratorSkillMd(), 'utf8');
2333
- await fsp.writeFile(js.file, buildInfographicGeneratorJs(), 'utf8');
2334
- } else {
2335
- if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
2336
- if (existsSync(js.file)) await fsp.rm(js.file, { force: true });
2337
- }
2338
- }
2339
-
2340
2356
  // Write cfgPath early so recreation reads updated openclaw.json
2341
2357
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2342
2358
 
@@ -2378,30 +2394,40 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2378
2394
  cfg.skills.entries['sticker-mention'].enabled = !!enabled;
2379
2395
 
2380
2396
  for (const a of cfg.agents.list) {
2381
- const sf = await readWorkspaceText(projectDir, a, 'skills/sticker-mention/SKILL.md');
2382
- const jsf = await readWorkspaceText(projectDir, a, 'skills/sticker-mention/mentions.js');
2383
- if (enabled) {
2384
- await fsp.mkdir(dirname(sf.file), { recursive: true });
2385
- await fsp.writeFile(sf.file, buildStickerMentionSkillMd(), 'utf8');
2386
- await fsp.writeFile(jsf.file, buildStickerMentionJs(), 'utf8');
2387
- } else {
2388
- const binding = (cfg.bindings || []).find((b) => b.agentId === a.id);
2389
- const channel = binding?.match?.channel || 'telegram';
2390
- if ((channel === 'zalo-personal' || channel === 'zalouser') && existsSync(jsf.file)) {
2391
- try {
2392
- const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2393
- if (hasDocker) {
2394
- const botContainer = getBotContainerName(projectDir);
2395
- const workspaceDir = workspaceRelForAgent(a, cfg, projectDir) || `workspace-${a.id}`;
2397
+ const binding = (cfg.bindings || []).find((b) => b.agentId === a.id);
2398
+ const channel = binding?.match?.channel || 'telegram';
2399
+ if (channel === 'zalo-personal' || channel === 'zalouser') {
2400
+ try {
2401
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2402
+ const workspaceDir = workspaceRelForAgent(a, cfg, projectDir) || `workspace-${a.id}`;
2403
+ let mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/zalo-sticker-mention/mentions.js');
2404
+ let containerJsPath = `/home/node/project/.openclaw/${workspaceDir}/skills/zalo-sticker-mention/mentions.js`;
2405
+ if (!existsSync(mentionsJsPath)) {
2406
+ mentionsJsPath = join(projectDir, '.openclaw', workspaceDir, 'skills/sticker-mention/mentions.js');
2407
+ containerJsPath = `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`;
2408
+ }
2409
+ if (existsSync(mentionsJsPath)) {
2410
+ if (enabled) {
2411
+ sendLog('[zalo-patch] Running patch on enable...');
2412
+ if (hasDocker) {
2413
+ const botContainer = getBotContainerName(projectDir);
2414
+ await runCapture('docker', ['exec', botContainer, 'node', containerJsPath], { cwd: projectDir, shell: false });
2415
+ } else {
2416
+ await run('node', [mentionsJsPath], { cwd: projectDir });
2417
+ }
2418
+ } else {
2396
2419
  sendLog('[zalo-patch] Running restore before disabling skill...');
2397
- await runCapture('docker', ['exec', botContainer, 'node', `/home/node/project/.openclaw/${workspaceDir}/skills/sticker-mention/mentions.js`, '--restore'], { cwd: projectDir, shell: false });
2420
+ if (hasDocker) {
2421
+ const botContainer = getBotContainerName(projectDir);
2422
+ await runCapture('docker', ['exec', botContainer, 'node', containerJsPath, '--restore'], { cwd: projectDir, shell: false });
2423
+ } else {
2424
+ await run('node', [mentionsJsPath, '--restore'], { cwd: projectDir });
2425
+ }
2398
2426
  }
2399
- } catch (e) {
2400
- sendLog(`[zalo-patch] Restore failed: ${e.message}`);
2401
2427
  }
2428
+ } catch (e) {
2429
+ sendLog(`[zalo-patch] Zalo patch/restore action failed: ${e.message}`);
2402
2430
  }
2403
- if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
2404
- if (existsSync(jsf.file)) await fsp.rm(jsf.file, { force: true });
2405
2431
  }
2406
2432
  }
2407
2433
 
@@ -2461,6 +2487,64 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2461
2487
  }
2462
2488
 
2463
2489
  async function installFeature(projectDir, agentId, kind, id) {
2490
+ if (kind === 'skill') {
2491
+ const skillSlugMap = {
2492
+ 'image-gen': 'infographic-generator',
2493
+ 'sticker-mention': 'zalo-sticker-mention',
2494
+ };
2495
+ const slug = skillSlugMap[id] || id;
2496
+
2497
+ let composeDir = null;
2498
+ if (existsSync(join(projectDir, 'docker-compose.yml'))) {
2499
+ composeDir = projectDir;
2500
+ } else if (existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'))) {
2501
+ composeDir = join(projectDir, 'docker', 'openclaw');
2502
+ }
2503
+
2504
+ if (composeDir) {
2505
+ const botContainer = getBotContainerName(projectDir);
2506
+ sendLog(`[skill] Installing/updating clawhub:${slug} inside container ${botContainer} for agent ${agentId}...`);
2507
+
2508
+ const cmd = `cd /home/node/project && openclaw skills install ${slug} --agent ${agentId} --force`;
2509
+ const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
2510
+
2511
+ if (cmdOut) {
2512
+ for (const line of `${cmdOut.stdout}\n${cmdOut.stderr}`.split(/\r?\n/).filter(Boolean)) sendLog(line);
2513
+ }
2514
+
2515
+ if (cmdOut.code !== 0) {
2516
+ const installed = isSkillFolderExists(projectDir, agentId, slug);
2517
+ if (installed) {
2518
+ sendLog(`[skill] Warning: installation reported errors, but skill folder exists. Proceeding.`);
2519
+ } else {
2520
+ throw new Error(cmdOut.stderr || cmdOut.stdout || `Failed to install skill ${slug} inside container.`);
2521
+ }
2522
+ }
2523
+
2524
+ sendLog(`[skill] Restarting docker container to apply skill...`);
2525
+ await run('docker', ['restart', botContainer], { shell: false });
2526
+ } else {
2527
+ await run('openclaw', ['doctor', '--fix'], { cwd: projectDir, env: openclawProjectEnv(projectDir) }).catch((err) => sendLog(`[skill] doctor --fix skipped: ${err.message}`));
2528
+ sendLog(`[skill] Installing clawhub:${slug} for agent ${agentId}...`);
2529
+
2530
+ await run('openclaw', ['skills', 'install', slug, '--agent', agentId, '--force'], {
2531
+ cwd: projectDir,
2532
+ env: openclawProjectEnv(projectDir)
2533
+ });
2534
+ }
2535
+
2536
+ // Automatically enable it in config after install
2537
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
2538
+ if (existsSync(cfgPath)) {
2539
+ const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
2540
+ cfg.skills = cfg.skills || { entries: {} };
2541
+ cfg.skills.entries = cfg.skills.entries || {};
2542
+ cfg.skills.entries[id] = cfg.skills.entries[id] || {};
2543
+ cfg.skills.entries[id].enabled = true;
2544
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2545
+ }
2546
+ }
2547
+
2464
2548
  if (kind === 'plugin') {
2465
2549
  let composeDir = null;
2466
2550
  if (existsSync(join(projectDir, 'docker-compose.yml'))) {
@@ -2624,6 +2708,48 @@ async function getInstalledPluginVersion(projectDir, aliases = []) {
2624
2708
  return '';
2625
2709
  }
2626
2710
 
2711
+ function isSkillFolderExists(projectDir, agentId, skillFolder, cfg = null) {
2712
+ if (!projectDir) return false;
2713
+ if (!cfg) {
2714
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
2715
+ if (!existsSync(cfgPath)) return false;
2716
+ try {
2717
+ cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
2718
+ } catch (e) {
2719
+ return false;
2720
+ }
2721
+ }
2722
+ const agent = cfg?.agents?.list?.find((a) => a.id === agentId) || cfg?.agents?.list?.[0];
2723
+ if (!agent) return false;
2724
+ const rel = workspaceRelForAgent(agent, cfg, projectDir);
2725
+ const skillPath = join(projectDir, '.openclaw', rel, 'skills', skillFolder);
2726
+ return existsSync(skillPath);
2727
+ }
2728
+
2729
+ async function getInstalledSkillVersion(projectDir, agentId, skillFolder, cfg = null) {
2730
+ if (!projectDir) return '';
2731
+ if (!cfg) {
2732
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
2733
+ if (!existsSync(cfgPath)) return '';
2734
+ try {
2735
+ cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
2736
+ } catch (e) {
2737
+ return '';
2738
+ }
2739
+ }
2740
+ const agent = cfg?.agents?.list?.find((a) => a.id === agentId) || cfg?.agents?.list?.[0];
2741
+ if (!agent) return '';
2742
+ const rel = workspaceRelForAgent(agent, cfg, projectDir);
2743
+ const pkgPath = join(projectDir, '.openclaw', rel, 'skills', skillFolder, 'package.json');
2744
+ try {
2745
+ if (existsSync(pkgPath)) {
2746
+ const pkg = JSON.parse(await fsp.readFile(pkgPath, 'utf8'));
2747
+ return pkg.version || '';
2748
+ }
2749
+ } catch (e) {}
2750
+ return '';
2751
+ }
2752
+
2627
2753
  async function getFeatureFlags(projectDir, agentId = '') {
2628
2754
  const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
2629
2755
  const cfg = existsSync(cfgPath) ? ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'))) : {};
@@ -2682,12 +2808,16 @@ async function getFeatureFlags(projectDir, agentId = '') {
2682
2808
  const isActuallyInstalled = (aliases = []) =>
2683
2809
  extensionDirExists(aliases) || isInstalledByRecord(aliases);
2684
2810
  const installed = {
2811
+ 'skill:image-gen': isSkillFolderExists(projectDir, aid, 'infographic-generator', cfg),
2812
+ 'skill:sticker-mention': isSkillFolderExists(projectDir, aid, 'zalo-sticker-mention', cfg),
2685
2813
  'plugin:openclaw-browser-automation': isActuallyInstalled(aliases.browser),
2686
2814
  'plugin:openclaw-zalo-mod': isActuallyInstalled(aliases.zalo),
2687
2815
  'plugin:openclaw-facebook-crawler': isActuallyInstalled(aliases.crawler),
2688
2816
  'plugin:openclaw-n8n-facebook-poster': isActuallyInstalled(aliases.poster),
2689
2817
  };
2690
2818
  const versions = {
2819
+ 'skill:image-gen': await getInstalledSkillVersion(projectDir, aid, 'infographic-generator', cfg),
2820
+ 'skill:sticker-mention': await getInstalledSkillVersion(projectDir, aid, 'zalo-sticker-mention', cfg),
2691
2821
  'plugin:openclaw-browser-automation': await getInstalledPluginVersion(projectDir, aliases.browser),
2692
2822
  'plugin:openclaw-zalo-mod': await getInstalledPluginVersion(projectDir, aliases.zalo),
2693
2823
  'plugin:openclaw-facebook-crawler': await getInstalledPluginVersion(projectDir, aliases.crawler),
@@ -53,7 +53,7 @@
53
53
  userInfo: '',
54
54
  securityRules: '',
55
55
  plugins: [],
56
- skills: ['memory', 'image-gen', 'web-search'],
56
+ skills: ['memory', 'web-search'],
57
57
  // Persisted credential inputs (Bug 1+2 fix)
58
58
  botToken: '',
59
59
  apiKey: '',
@@ -304,6 +304,13 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
304
304
  ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
305
305
  ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
306
306
  'fi',
307
+ '# Automatically run Zalo sticker-mention patch if skill is present',
308
+ 'for MENTIONS_JS in $(find "$OPENCLAW_HOME" -maxdepth 5 -path "*/skills/*/mentions.js" -type f 2>/dev/null); do',
309
+ ' if [ -f "$MENTIONS_JS" ]; then',
310
+ ' echo "[entrypoint] Running patch: $MENTIONS_JS"',
311
+ ' node "$MENTIONS_JS" || echo "[entrypoint] Warning: failed to run patch $MENTIONS_JS"',
312
+ ' fi',
313
+ 'done',
307
314
  ].join('\n'));
308
315
  runtimeParts.push([
309
316
  '# Zalo channel auto-restart monitor (background)',