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 +3 -3
- package/README.vi.md +2 -2
- package/dist/server/local-server.js +237 -107
- package/dist/setup/data/header.js +1 -1
- package/dist/setup/shared/docker-gen.js +7 -0
- package/dist/setup/shared/workspace-gen.js +16 -821
- package/dist/web/app.js +30 -9
- package/package.json +1 -1
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.
|
|
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.
|
|
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 [
|
|
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.
|
|
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.
|
|
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 ? `- **
|
|
1078
|
+
const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô 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', '
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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',
|
|
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', '
|
|
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', '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
(
|
|
2107
|
-
(
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
|
2382
|
-
const
|
|
2383
|
-
if (
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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
|
-
|
|
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),
|
|
@@ -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)',
|