create-openclaw-bot 5.8.20 → 5.8.23
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 +14 -16
- package/README.vi.md +14 -16
- package/dist/cli.js +12 -8
- package/dist/server/local-server.js +473 -417
- package/dist/setup/shared/bot-config-gen.js +41 -8
- package/dist/setup/shared/docker-gen.js +11 -9
- package/dist/web/app.js +73 -72
- package/package.json +1 -1
|
@@ -19,7 +19,7 @@ const { buildDockerArtifacts } = loadSharedModule('../setup/shared/docker-gen.js
|
|
|
19
19
|
const { OPENCLAW_NPM_SPEC, NINE_ROUTER_NPM_SPEC, build9RouterProviderConfig, get9RouterBaseUrl } = loadSharedModule('../setup/shared/common-gen.js', '__openclawCommon');
|
|
20
20
|
const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
|
|
21
21
|
|
|
22
|
-
async function syncExecApprovals(projectDir, cfg) {
|
|
22
|
+
async function syncExecApprovals(projectDir, cfg) {
|
|
23
23
|
const openclawHome = join(projectDir, '.openclaw');
|
|
24
24
|
const agentMetas = (cfg.agents?.list || []).map((a) => ({ agentId: a.id }));
|
|
25
25
|
const approvals = buildExecApprovalsJson({ agentMetas });
|
|
@@ -41,189 +41,189 @@ async function syncExecApprovals(projectDir, cfg) {
|
|
|
41
41
|
if (existing.socket) {
|
|
42
42
|
approvals.socket = existing.socket;
|
|
43
43
|
}
|
|
44
|
-
await fsp.writeFile(path2, JSON.stringify(approvals, null, 2), 'utf8');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function patchBrowserAutomationHostPreference(projectDir, aliases = [], sendLog = () => {}) {
|
|
48
|
-
const preferredCdpBlock = `const dns = require('dns').promises;
|
|
49
|
-
const DEFAULT_CDP_URLS = [
|
|
50
|
-
'host-gateway:9222',
|
|
51
|
-
'http://127.0.0.1:9222',
|
|
52
|
-
];
|
|
53
|
-
const CDP_URLS = (process.env.OPENCLAW_BROWSER_CDP_URLS || DEFAULT_CDP_URLS.join(','))
|
|
54
|
-
.split(',')
|
|
55
|
-
.map((u) => u.trim())
|
|
56
|
-
.filter(Boolean);
|
|
57
|
-
|
|
58
|
-
async function normalizeCdpUrl(url) {
|
|
59
|
-
if (url === 'host-gateway:9222') {
|
|
60
|
-
try {
|
|
61
|
-
const resolved = await dns.lookup('host.docker.internal');
|
|
62
|
-
return 'http://' + resolved.address + ':9222';
|
|
63
|
-
} catch (_) {
|
|
64
|
-
return 'http://host.docker.internal:9222';
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return url;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function connectPreferredChrome() {
|
|
71
|
-
let lastError;
|
|
72
|
-
for (const rawUrl of CDP_URLS) {
|
|
73
|
-
const url = await normalizeCdpUrl(rawUrl);
|
|
74
|
-
try {
|
|
75
|
-
const connected = await chromium.connectOverCDP(url, { timeout: 2500 });
|
|
76
|
-
console.error('[Browser] Connected CDP: ' + url);
|
|
77
|
-
return connected;
|
|
78
|
-
} catch (err) {
|
|
79
|
-
lastError = err;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
throw lastError || new Error('No Chrome CDP endpoint available');
|
|
83
|
-
}`;
|
|
84
|
-
|
|
85
|
-
const patchContent = (content) => {
|
|
86
|
-
let next = content;
|
|
87
|
-
next = next.replace(
|
|
88
|
-
"const CDP_URL = 'http://127.0.0.1:9222';",
|
|
89
|
-
preferredCdpBlock
|
|
90
|
-
);
|
|
91
|
-
next = next.replace(
|
|
92
|
-
'browser = await chromium.connectOverCDP(CDP_URL, { timeout: 5000 });',
|
|
93
|
-
'browser = await connectPreferredChrome();'
|
|
94
|
-
);
|
|
95
|
-
return next;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const browserToolCandidates = new Set();
|
|
99
|
-
const extensionDirs = [];
|
|
100
|
-
for (const alias of aliases) {
|
|
101
|
-
const extensionDir = join(projectDir, '.openclaw', 'extensions', alias);
|
|
102
|
-
extensionDirs.push(extensionDir);
|
|
103
|
-
browserToolCandidates.add(join(extensionDir, 'browser-tool.js'));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const workspaceDirs = new Set();
|
|
107
|
-
try {
|
|
108
|
-
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
109
|
-
if (existsSync(cfgPath)) {
|
|
110
|
-
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
111
|
-
for (const a of cfg.agents?.list || []) {
|
|
112
|
-
const workspaceRel = a.workspace || cfg.agents?.defaults?.workspace;
|
|
113
|
-
if (!workspaceRel) continue;
|
|
114
|
-
const workspacePath = workspaceRel.startsWith('/') ? join(projectDir, workspaceRel.replace(/^\/home\/node\/project\/?/, '')) : join(projectDir, workspaceRel);
|
|
115
|
-
workspaceDirs.add(workspacePath);
|
|
116
|
-
browserToolCandidates.add(join(workspacePath, 'plugin-skills', 'browser-automation', 'browser-tool.js'));
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
120
|
-
sendLog(`[browser] Warning: could not scan workspaces for browser-tool.js: ${err.message}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let patched = 0;
|
|
124
|
-
for (const file of browserToolCandidates) {
|
|
125
|
-
if (!existsSync(file)) continue;
|
|
126
|
-
const content = await fsp.readFile(file, 'utf8');
|
|
127
|
-
if (content.includes('connectPreferredChrome')) continue;
|
|
128
|
-
const next = patchContent(content);
|
|
129
|
-
if (next !== content) {
|
|
130
|
-
await fsp.writeFile(file, next, 'utf8');
|
|
131
|
-
patched += 1;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (patched > 0) {
|
|
135
|
-
sendLog(`[browser] Patched ${patched} browser-tool.js file(s) to prefer host Chrome debug before headless Chromium.`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const browserMd = `# Browser Automation
|
|
139
|
-
|
|
140
|
-
This plugin skill owns browser automation only. For normal web search, use OpenClaw's built-in \`web_search\` capability.
|
|
141
|
-
|
|
142
|
-
Run commands from this folder or pass the full path from the workspace root:
|
|
143
|
-
|
|
144
|
-
- \`cd plugin-skills/browser-automation && node browser-tool.js status\`
|
|
145
|
-
- \`node plugin-skills/browser-automation/browser-tool.js status\`
|
|
146
|
-
|
|
147
|
-
## Chrome Debug Mode
|
|
148
|
-
|
|
149
|
-
On a desktop machine, start real Chrome in debug mode before asking the bot to browse:
|
|
150
|
-
|
|
151
|
-
- Windows: run \`start-chrome-debug.bat\`
|
|
152
|
-
- macOS/Linux: run \`./start-chrome-debug.sh\`
|
|
153
|
-
|
|
154
|
-
The tool will try real host Chrome first. If Chrome debug is not available, it falls back to local headless Chromium, which is suitable for VPS/server use.
|
|
155
|
-
|
|
156
|
-
## Browser Commands
|
|
157
|
-
|
|
158
|
-
- \`node plugin-skills/browser-automation/browser-tool.js status\`: check the active browser/tab
|
|
159
|
-
- \`node plugin-skills/browser-automation/browser-tool.js open <url>\`: open a page
|
|
160
|
-
- \`node plugin-skills/browser-automation/browser-tool.js get_text [max_chars]\`: read rendered page text
|
|
161
|
-
- \`node plugin-skills/browser-automation/browser-tool.js get_links [filter]\`: list links
|
|
162
|
-
- \`node plugin-skills/browser-automation/browser-tool.js click "<selector>"\`: click an element
|
|
163
|
-
- \`node plugin-skills/browser-automation/browser-tool.js fill "<selector>" "<text>"\`: fill an input
|
|
164
|
-
- \`node plugin-skills/browser-automation/browser-tool.js scroll [px]\`: scroll the page
|
|
165
|
-
- \`node plugin-skills/browser-automation/browser-tool.js screenshot [path]\`: capture the viewport
|
|
166
|
-
- \`node plugin-skills/browser-automation/browser-tool.js tabs\`: list tabs
|
|
167
|
-
|
|
168
|
-
Do not call \`search-tool.js\`; browser-automation does not own search. Use \`web_search\` for search and this browser tool only when a rendered browser is needed.
|
|
169
|
-
`;
|
|
170
|
-
|
|
171
|
-
const removeManagedBlock = (content, blockId) => {
|
|
172
|
-
const startTag = `<!-- OPENCLAW:${blockId}:START -->`;
|
|
173
|
-
const endTag = `<!-- OPENCLAW:${blockId}:END -->`;
|
|
174
|
-
const startIdx = content.indexOf(startTag);
|
|
175
|
-
const endIdx = content.indexOf(endTag);
|
|
176
|
-
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return content;
|
|
177
|
-
return `${content.substring(0, startIdx).trimEnd()}\n${content.substring(endIdx + endTag.length).trimStart()}`.trim() + '\n';
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const hostOs = normalizeHostOs(await resolveProjectHostOs(projectDir));
|
|
181
|
-
const shouldKeepBat = hostOs === 'win';
|
|
182
|
-
const scriptToKeep = shouldKeepBat ? 'start-chrome-debug.bat' : 'start-chrome-debug.sh';
|
|
183
|
-
const scriptToRemove = shouldKeepBat ? 'start-chrome-debug.sh' : 'start-chrome-debug.bat';
|
|
184
|
-
const sourceScript = extensionDirs.map((dir) => join(dir, scriptToKeep)).find((file) => existsSync(file));
|
|
185
|
-
const sourceBrowserTool = extensionDirs.map((dir) => join(dir, 'browser-tool.js')).find((file) => existsSync(file));
|
|
186
|
-
|
|
187
|
-
let sanitized = 0;
|
|
188
|
-
for (const workspacePath of workspaceDirs) {
|
|
189
|
-
if (!existsSync(workspacePath)) continue;
|
|
190
|
-
const pluginSkillPath = join(workspacePath, 'plugin-skills', 'browser-automation');
|
|
191
|
-
await fsp.mkdir(pluginSkillPath, { recursive: true }).catch(() => {});
|
|
192
|
-
await fsp.rm(join(workspacePath, 'search-tool.js'), { force: true }).catch(() => {});
|
|
193
|
-
await fsp.rm(join(workspacePath, 'browser-tool.js'), { force: true }).catch(() => {});
|
|
194
|
-
await fsp.rm(join(workspacePath, 'BROWSER.md'), { force: true }).catch(() => {});
|
|
195
|
-
await fsp.rm(join(workspacePath, scriptToRemove), { force: true }).catch(() => {});
|
|
196
|
-
await fsp.rm(join(workspacePath, scriptToKeep), { force: true }).catch(() => {});
|
|
197
|
-
await fsp.rm(join(pluginSkillPath, scriptToRemove), { force: true }).catch(() => {});
|
|
198
|
-
if (sourceBrowserTool) {
|
|
199
|
-
const targetBrowserTool = join(pluginSkillPath, 'browser-tool.js');
|
|
200
|
-
await fsp.copyFile(sourceBrowserTool, targetBrowserTool).catch(() => {});
|
|
201
|
-
if (existsSync(targetBrowserTool)) {
|
|
202
|
-
const content = await fsp.readFile(targetBrowserTool, 'utf8');
|
|
203
|
-
const next = content.includes('connectPreferredChrome') ? content : patchContent(content);
|
|
204
|
-
if (next !== content) await fsp.writeFile(targetBrowserTool, next, 'utf8');
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (sourceScript) {
|
|
208
|
-
await fsp.copyFile(sourceScript, join(pluginSkillPath, scriptToKeep)).catch(() => {});
|
|
209
|
-
if (scriptToKeep.endsWith('.sh')) await fsp.chmod(join(pluginSkillPath, scriptToKeep), 0o755).catch(() => {});
|
|
210
|
-
}
|
|
211
|
-
await fsp.writeFile(join(pluginSkillPath, 'BROWSER.md'), browserMd, 'utf8');
|
|
212
|
-
for (const dirName of ['cl-stealth-search', 'openclaw-smart-search']) {
|
|
213
|
-
await fsp.rm(join(workspacePath, 'plugin-skills', dirName), { recursive: true, force: true }).catch(() => {});
|
|
214
|
-
}
|
|
215
|
-
const toolsMdPath = join(workspacePath, 'TOOLS.md');
|
|
216
|
-
if (existsSync(toolsMdPath)) {
|
|
217
|
-
const toolsContent = await fsp.readFile(toolsMdPath, 'utf8');
|
|
218
|
-
const cleaned = removeManagedBlock(toolsContent, 'STEALTH_BROWSER_GUIDE');
|
|
219
|
-
if (cleaned !== toolsContent) await fsp.writeFile(toolsMdPath, cleaned, 'utf8');
|
|
220
|
-
}
|
|
221
|
-
sanitized += 1;
|
|
222
|
-
}
|
|
223
|
-
if (sanitized > 0) {
|
|
224
|
-
sendLog(`[browser] Sanitized ${sanitized} workspace(s): browser assets are in plugin-skills/browser-automation; web_search remains responsible for search.`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
44
|
+
await fsp.writeFile(path2, JSON.stringify(approvals, null, 2), 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function patchBrowserAutomationHostPreference(projectDir, aliases = [], sendLog = () => {}) {
|
|
48
|
+
const preferredCdpBlock = `const dns = require('dns').promises;
|
|
49
|
+
const DEFAULT_CDP_URLS = [
|
|
50
|
+
'host-gateway:9222',
|
|
51
|
+
'http://127.0.0.1:9222',
|
|
52
|
+
];
|
|
53
|
+
const CDP_URLS = (process.env.OPENCLAW_BROWSER_CDP_URLS || DEFAULT_CDP_URLS.join(','))
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((u) => u.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
|
|
58
|
+
async function normalizeCdpUrl(url) {
|
|
59
|
+
if (url === 'host-gateway:9222') {
|
|
60
|
+
try {
|
|
61
|
+
const resolved = await dns.lookup('host.docker.internal');
|
|
62
|
+
return 'http://' + resolved.address + ':9222';
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return 'http://host.docker.internal:9222';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function connectPreferredChrome() {
|
|
71
|
+
let lastError;
|
|
72
|
+
for (const rawUrl of CDP_URLS) {
|
|
73
|
+
const url = await normalizeCdpUrl(rawUrl);
|
|
74
|
+
try {
|
|
75
|
+
const connected = await chromium.connectOverCDP(url, { timeout: 2500 });
|
|
76
|
+
console.error('[Browser] Connected CDP: ' + url);
|
|
77
|
+
return connected;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
lastError = err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw lastError || new Error('No Chrome CDP endpoint available');
|
|
83
|
+
}`;
|
|
84
|
+
|
|
85
|
+
const patchContent = (content) => {
|
|
86
|
+
let next = content;
|
|
87
|
+
next = next.replace(
|
|
88
|
+
"const CDP_URL = 'http://127.0.0.1:9222';",
|
|
89
|
+
preferredCdpBlock
|
|
90
|
+
);
|
|
91
|
+
next = next.replace(
|
|
92
|
+
'browser = await chromium.connectOverCDP(CDP_URL, { timeout: 5000 });',
|
|
93
|
+
'browser = await connectPreferredChrome();'
|
|
94
|
+
);
|
|
95
|
+
return next;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const browserToolCandidates = new Set();
|
|
99
|
+
const extensionDirs = [];
|
|
100
|
+
for (const alias of aliases) {
|
|
101
|
+
const extensionDir = join(projectDir, '.openclaw', 'extensions', alias);
|
|
102
|
+
extensionDirs.push(extensionDir);
|
|
103
|
+
browserToolCandidates.add(join(extensionDir, 'browser-tool.js'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const workspaceDirs = new Set();
|
|
107
|
+
try {
|
|
108
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
109
|
+
if (existsSync(cfgPath)) {
|
|
110
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
111
|
+
for (const a of cfg.agents?.list || []) {
|
|
112
|
+
const workspaceRel = a.workspace || cfg.agents?.defaults?.workspace;
|
|
113
|
+
if (!workspaceRel) continue;
|
|
114
|
+
const workspacePath = workspaceRel.startsWith('/') ? join(projectDir, workspaceRel.replace(/^\/home\/node\/project\/?/, '')) : join(projectDir, workspaceRel);
|
|
115
|
+
workspaceDirs.add(workspacePath);
|
|
116
|
+
browserToolCandidates.add(join(workspacePath, 'plugin-skills', 'browser-automation', 'browser-tool.js'));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
sendLog(`[browser] Warning: could not scan workspaces for browser-tool.js: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let patched = 0;
|
|
124
|
+
for (const file of browserToolCandidates) {
|
|
125
|
+
if (!existsSync(file)) continue;
|
|
126
|
+
const content = await fsp.readFile(file, 'utf8');
|
|
127
|
+
if (content.includes('connectPreferredChrome')) continue;
|
|
128
|
+
const next = patchContent(content);
|
|
129
|
+
if (next !== content) {
|
|
130
|
+
await fsp.writeFile(file, next, 'utf8');
|
|
131
|
+
patched += 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (patched > 0) {
|
|
135
|
+
sendLog(`[browser] Patched ${patched} browser-tool.js file(s) to prefer host Chrome debug before headless Chromium.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const browserMd = `# Browser Automation
|
|
139
|
+
|
|
140
|
+
This plugin skill owns browser automation only. For normal web search, use OpenClaw's built-in \`web_search\` capability.
|
|
141
|
+
|
|
142
|
+
Run commands from this folder or pass the full path from the workspace root:
|
|
143
|
+
|
|
144
|
+
- \`cd plugin-skills/browser-automation && node browser-tool.js status\`
|
|
145
|
+
- \`node plugin-skills/browser-automation/browser-tool.js status\`
|
|
146
|
+
|
|
147
|
+
## Chrome Debug Mode
|
|
148
|
+
|
|
149
|
+
On a desktop machine, start real Chrome in debug mode before asking the bot to browse:
|
|
150
|
+
|
|
151
|
+
- Windows: run \`start-chrome-debug.bat\`
|
|
152
|
+
- macOS/Linux: run \`./start-chrome-debug.sh\`
|
|
153
|
+
|
|
154
|
+
The tool will try real host Chrome first. If Chrome debug is not available, it falls back to local headless Chromium, which is suitable for VPS/server use.
|
|
155
|
+
|
|
156
|
+
## Browser Commands
|
|
157
|
+
|
|
158
|
+
- \`node plugin-skills/browser-automation/browser-tool.js status\`: check the active browser/tab
|
|
159
|
+
- \`node plugin-skills/browser-automation/browser-tool.js open <url>\`: open a page
|
|
160
|
+
- \`node plugin-skills/browser-automation/browser-tool.js get_text [max_chars]\`: read rendered page text
|
|
161
|
+
- \`node plugin-skills/browser-automation/browser-tool.js get_links [filter]\`: list links
|
|
162
|
+
- \`node plugin-skills/browser-automation/browser-tool.js click "<selector>"\`: click an element
|
|
163
|
+
- \`node plugin-skills/browser-automation/browser-tool.js fill "<selector>" "<text>"\`: fill an input
|
|
164
|
+
- \`node plugin-skills/browser-automation/browser-tool.js scroll [px]\`: scroll the page
|
|
165
|
+
- \`node plugin-skills/browser-automation/browser-tool.js screenshot [path]\`: capture the viewport
|
|
166
|
+
- \`node plugin-skills/browser-automation/browser-tool.js tabs\`: list tabs
|
|
167
|
+
|
|
168
|
+
Do not call \`search-tool.js\`; browser-automation does not own search. Use \`web_search\` for search and this browser tool only when a rendered browser is needed.
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
const removeManagedBlock = (content, blockId) => {
|
|
172
|
+
const startTag = `<!-- OPENCLAW:${blockId}:START -->`;
|
|
173
|
+
const endTag = `<!-- OPENCLAW:${blockId}:END -->`;
|
|
174
|
+
const startIdx = content.indexOf(startTag);
|
|
175
|
+
const endIdx = content.indexOf(endTag);
|
|
176
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return content;
|
|
177
|
+
return `${content.substring(0, startIdx).trimEnd()}\n${content.substring(endIdx + endTag.length).trimStart()}`.trim() + '\n';
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const hostOs = normalizeHostOs(await resolveProjectHostOs(projectDir));
|
|
181
|
+
const shouldKeepBat = hostOs === 'win';
|
|
182
|
+
const scriptToKeep = shouldKeepBat ? 'start-chrome-debug.bat' : 'start-chrome-debug.sh';
|
|
183
|
+
const scriptToRemove = shouldKeepBat ? 'start-chrome-debug.sh' : 'start-chrome-debug.bat';
|
|
184
|
+
const sourceScript = extensionDirs.map((dir) => join(dir, scriptToKeep)).find((file) => existsSync(file));
|
|
185
|
+
const sourceBrowserTool = extensionDirs.map((dir) => join(dir, 'browser-tool.js')).find((file) => existsSync(file));
|
|
186
|
+
|
|
187
|
+
let sanitized = 0;
|
|
188
|
+
for (const workspacePath of workspaceDirs) {
|
|
189
|
+
if (!existsSync(workspacePath)) continue;
|
|
190
|
+
const pluginSkillPath = join(workspacePath, 'plugin-skills', 'browser-automation');
|
|
191
|
+
await fsp.mkdir(pluginSkillPath, { recursive: true }).catch(() => {});
|
|
192
|
+
await fsp.rm(join(workspacePath, 'search-tool.js'), { force: true }).catch(() => {});
|
|
193
|
+
await fsp.rm(join(workspacePath, 'browser-tool.js'), { force: true }).catch(() => {});
|
|
194
|
+
await fsp.rm(join(workspacePath, 'BROWSER.md'), { force: true }).catch(() => {});
|
|
195
|
+
await fsp.rm(join(workspacePath, scriptToRemove), { force: true }).catch(() => {});
|
|
196
|
+
await fsp.rm(join(workspacePath, scriptToKeep), { force: true }).catch(() => {});
|
|
197
|
+
await fsp.rm(join(pluginSkillPath, scriptToRemove), { force: true }).catch(() => {});
|
|
198
|
+
if (sourceBrowserTool) {
|
|
199
|
+
const targetBrowserTool = join(pluginSkillPath, 'browser-tool.js');
|
|
200
|
+
await fsp.copyFile(sourceBrowserTool, targetBrowserTool).catch(() => {});
|
|
201
|
+
if (existsSync(targetBrowserTool)) {
|
|
202
|
+
const content = await fsp.readFile(targetBrowserTool, 'utf8');
|
|
203
|
+
const next = content.includes('connectPreferredChrome') ? content : patchContent(content);
|
|
204
|
+
if (next !== content) await fsp.writeFile(targetBrowserTool, next, 'utf8');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (sourceScript) {
|
|
208
|
+
await fsp.copyFile(sourceScript, join(pluginSkillPath, scriptToKeep)).catch(() => {});
|
|
209
|
+
if (scriptToKeep.endsWith('.sh')) await fsp.chmod(join(pluginSkillPath, scriptToKeep), 0o755).catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
await fsp.writeFile(join(pluginSkillPath, 'BROWSER.md'), browserMd, 'utf8');
|
|
212
|
+
for (const dirName of ['cl-stealth-search', 'openclaw-smart-search']) {
|
|
213
|
+
await fsp.rm(join(workspacePath, 'plugin-skills', dirName), { recursive: true, force: true }).catch(() => {});
|
|
214
|
+
}
|
|
215
|
+
const toolsMdPath = join(workspacePath, 'TOOLS.md');
|
|
216
|
+
if (existsSync(toolsMdPath)) {
|
|
217
|
+
const toolsContent = await fsp.readFile(toolsMdPath, 'utf8');
|
|
218
|
+
const cleaned = removeManagedBlock(toolsContent, 'STEALTH_BROWSER_GUIDE');
|
|
219
|
+
if (cleaned !== toolsContent) await fsp.writeFile(toolsMdPath, cleaned, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
sanitized += 1;
|
|
222
|
+
}
|
|
223
|
+
if (sanitized > 0) {
|
|
224
|
+
sendLog(`[browser] Sanitized ${sanitized} workspace(s): browser assets are in plugin-skills/browser-automation; web_search remains responsible for search.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
227
|
|
|
228
228
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
229
229
|
const WEB_DIR = resolve(__dirname, '../web');
|
|
@@ -295,39 +295,39 @@ function extractCompletePngBase64(stdout) {
|
|
|
295
295
|
return b64;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
function detectOs() {
|
|
299
|
-
const platform = process.platform;
|
|
300
|
-
if (platform === 'win32') return 'win';
|
|
301
|
-
if (platform === 'darwin') return 'macos';
|
|
302
|
-
if (platform === 'linux') return os.release().toLowerCase().includes('microsoft') ? 'linux-desktop' : 'linux-desktop';
|
|
303
|
-
return 'linux-desktop';
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function normalizeHostOs(value = '') {
|
|
307
|
-
const v = String(value || '').trim().toLowerCase();
|
|
308
|
-
if (['win', 'windows', 'win32'].includes(v)) return 'win';
|
|
309
|
-
if (['mac', 'macos', 'darwin'].includes(v)) return 'macos';
|
|
310
|
-
if (['vps', 'server'].includes(v)) return 'vps';
|
|
311
|
-
if (['linux', 'linux-desktop', 'ubuntu', 'debian'].includes(v)) return 'linux-desktop';
|
|
312
|
-
return '';
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function resolveProjectHostOs(projectDir = '') {
|
|
316
|
-
try {
|
|
317
|
-
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
318
|
-
if (existsSync(cfgPath)) {
|
|
319
|
-
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
320
|
-
const pluginEntries = cfg.plugins?.entries || {};
|
|
321
|
-
const browserEntry = pluginEntries['browser-automation'] || pluginEntries['openclaw-browser-automation'] || {};
|
|
322
|
-
const fromConfig = normalizeHostOs(browserEntry.config?.hostOs || cfg.meta?.osChoice || cfg.meta?.hostOs);
|
|
323
|
-
if (fromConfig) return fromConfig;
|
|
324
|
-
}
|
|
325
|
-
} catch {}
|
|
326
|
-
const fromState = normalizeHostOs(state.os);
|
|
327
|
-
if (fromState) return fromState;
|
|
328
|
-
if (/^[A-Za-z]:[\\/]/.test(String(projectDir || ''))) return 'win';
|
|
329
|
-
return detectOs();
|
|
330
|
-
}
|
|
298
|
+
function detectOs() {
|
|
299
|
+
const platform = process.platform;
|
|
300
|
+
if (platform === 'win32') return 'win';
|
|
301
|
+
if (platform === 'darwin') return 'macos';
|
|
302
|
+
if (platform === 'linux') return os.release().toLowerCase().includes('microsoft') ? 'linux-desktop' : 'linux-desktop';
|
|
303
|
+
return 'linux-desktop';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizeHostOs(value = '') {
|
|
307
|
+
const v = String(value || '').trim().toLowerCase();
|
|
308
|
+
if (['win', 'windows', 'win32'].includes(v)) return 'win';
|
|
309
|
+
if (['mac', 'macos', 'darwin'].includes(v)) return 'macos';
|
|
310
|
+
if (['vps', 'server'].includes(v)) return 'vps';
|
|
311
|
+
if (['linux', 'linux-desktop', 'ubuntu', 'debian'].includes(v)) return 'linux-desktop';
|
|
312
|
+
return '';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function resolveProjectHostOs(projectDir = '') {
|
|
316
|
+
try {
|
|
317
|
+
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
318
|
+
if (existsSync(cfgPath)) {
|
|
319
|
+
const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
|
|
320
|
+
const pluginEntries = cfg.plugins?.entries || {};
|
|
321
|
+
const browserEntry = pluginEntries['browser-automation'] || pluginEntries['openclaw-browser-automation'] || {};
|
|
322
|
+
const fromConfig = normalizeHostOs(browserEntry.config?.hostOs || cfg.meta?.osChoice || cfg.meta?.hostOs);
|
|
323
|
+
if (fromConfig) return fromConfig;
|
|
324
|
+
}
|
|
325
|
+
} catch {}
|
|
326
|
+
const fromState = normalizeHostOs(state.os);
|
|
327
|
+
if (fromState) return fromState;
|
|
328
|
+
if (/^[A-Za-z]:[\\/]/.test(String(projectDir || ''))) return 'win';
|
|
329
|
+
return detectOs();
|
|
330
|
+
}
|
|
331
331
|
|
|
332
332
|
function getRealHomedir() {
|
|
333
333
|
const home = os.homedir();
|
|
@@ -617,6 +617,12 @@ function openclawProjectEnv(projectDir) {
|
|
|
617
617
|
};
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
function parseJsonText(text, fallback = undefined) {
|
|
621
|
+
const clean = String(text || '').replace(/^\uFEFF/, '');
|
|
622
|
+
if (!clean.trim() && fallback !== undefined) return fallback;
|
|
623
|
+
return JSON.parse(clean);
|
|
624
|
+
}
|
|
625
|
+
|
|
620
626
|
async function runOpenclawJson(projectDir, args = [], timeout = 12000) {
|
|
621
627
|
const out = await runCapture('openclaw', args, {
|
|
622
628
|
cwd: projectDir,
|
|
@@ -626,7 +632,7 @@ async function runOpenclawJson(projectDir, args = [], timeout = 12000) {
|
|
|
626
632
|
});
|
|
627
633
|
if (out.code !== 0) throw new Error((out.stderr || out.stdout || `openclaw ${args.join(' ')} failed`).trim());
|
|
628
634
|
const text = String(out.stdout || '').trim();
|
|
629
|
-
return text ?
|
|
635
|
+
return text ? parseJsonText(text) : null;
|
|
630
636
|
}
|
|
631
637
|
|
|
632
638
|
async function readComposeText(projectDir) {
|
|
@@ -674,7 +680,7 @@ function parseBaseUrlPort(baseUrl = '') {
|
|
|
674
680
|
|
|
675
681
|
async function detectRuntime(projectDir) {
|
|
676
682
|
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
677
|
-
const cfg = existsSync(cfgPath) ?
|
|
683
|
+
const cfg = existsSync(cfgPath) ? parseJsonText(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'), {}) : {};
|
|
678
684
|
let cliGatewayStatus = null;
|
|
679
685
|
let cliGatewayPort = 0;
|
|
680
686
|
let cliRouterPort = 0;
|
|
@@ -1043,7 +1049,7 @@ async function applyResolved9RouterApiKey(projectDir, cfg = null) {
|
|
|
1043
1049
|
if (!projectDir) return '';
|
|
1044
1050
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
1045
1051
|
if (!existsSync(cfgPath)) return '';
|
|
1046
|
-
const current = cfg || ensureConfigShape(
|
|
1052
|
+
const current = cfg || ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8')));
|
|
1047
1053
|
const apiKey = await resolveProject9RouterApiKey(projectDir, current);
|
|
1048
1054
|
if (!apiKey) return '';
|
|
1049
1055
|
current.models = current.models || { mode: 'merge', providers: {} };
|
|
@@ -1059,7 +1065,7 @@ async function applyResolved9RouterApiKey(projectDir, cfg = null) {
|
|
|
1059
1065
|
async function readBotCredentials(projectDir) {
|
|
1060
1066
|
const found = readProjectConfig(projectDir);
|
|
1061
1067
|
if (!found) return { openclawToken: '', nineRouterApiKey: '' };
|
|
1062
|
-
const cfg = ensureConfigShape(
|
|
1068
|
+
const cfg = ensureConfigShape(parseJsonText(await fsp.readFile(found.cfgPath, 'utf8')));
|
|
1063
1069
|
return {
|
|
1064
1070
|
openclawToken: cfg.gateway?.auth?.token || '',
|
|
1065
1071
|
nineRouterApiKey: await resolveProject9RouterApiKey(projectDir, cfg),
|
|
@@ -1070,7 +1076,7 @@ async function updateBotCredentials(projectDir, body = {}) {
|
|
|
1070
1076
|
const found = readProjectConfig(projectDir);
|
|
1071
1077
|
if (!found) throw httpError(400, 'Install project not found');
|
|
1072
1078
|
const raw = await fsp.readFile(found.cfgPath, 'utf8');
|
|
1073
|
-
const cfg = ensureConfigShape(
|
|
1079
|
+
const cfg = ensureConfigShape(parseJsonText(raw));
|
|
1074
1080
|
const nineRouterApiKey = String(body.nineRouterApiKey || '').trim();
|
|
1075
1081
|
if (Object.prototype.hasOwnProperty.call(body, 'nineRouterApiKey')) {
|
|
1076
1082
|
cfg.models = cfg.models || { mode: 'merge', providers: {} };
|
|
@@ -1142,7 +1148,7 @@ async function listConfiguredBots(projectDir) {
|
|
|
1142
1148
|
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
1143
1149
|
if (!projectDir || !existsSync(cfgPath)) return [];
|
|
1144
1150
|
const raw = await fsp.readFile(cfgPath, 'utf8');
|
|
1145
|
-
const cfg = ensureConfigShape(
|
|
1151
|
+
const cfg = ensureConfigShape(parseJsonText(raw));
|
|
1146
1152
|
const normalized = JSON.stringify(cfg, null, 2) + '\n';
|
|
1147
1153
|
if (normalized !== raw) await fsp.writeFile(cfgPath, normalized, 'utf8');
|
|
1148
1154
|
const rows = await Promise.all(cfg.agents.list.map(async (agent) => {
|
|
@@ -1177,7 +1183,7 @@ async function deleteBotInProject(projectDir, agentId) {
|
|
|
1177
1183
|
const openclawHome = join(projectDir, '.openclaw');
|
|
1178
1184
|
const cfgPath = join(openclawHome, 'openclaw.json');
|
|
1179
1185
|
if (!existsSync(cfgPath)) throw httpError(404, 'openclaw.json not found');
|
|
1180
|
-
const cfg = ensureConfigShape(
|
|
1186
|
+
const cfg = ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8')));
|
|
1181
1187
|
const agent = cfg.agents.list.find((a) => a.id === agentId);
|
|
1182
1188
|
if (!agent) throw httpError(404, 'Bot not found');
|
|
1183
1189
|
|
|
@@ -1815,9 +1821,9 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1815
1821
|
const botContainer = parseComposeServiceContainerName(compose, 'ai-bot') || `openclaw-${slugify(basename(projectDir))}`;
|
|
1816
1822
|
const routerContainer = parseComposeServiceContainerName(compose, '9router') || `9router-${slugify(basename(projectDir))}`;
|
|
1817
1823
|
const composeName = (compose.match(/^name:\s*(\S+)/m) || [])[1] || `oc-${slugify(basename(projectDir))}`;
|
|
1818
|
-
const gatewayPort = state.gatewayPort || 18789;
|
|
1819
|
-
const routerPort = state.routerPort || 20128;
|
|
1820
|
-
const osChoice = await resolveProjectHostOs(projectDir);
|
|
1824
|
+
const gatewayPort = state.gatewayPort || 18789;
|
|
1825
|
+
const routerPort = state.routerPort || 20128;
|
|
1826
|
+
const osChoice = await resolveProjectHostOs(projectDir);
|
|
1821
1827
|
|
|
1822
1828
|
// Detect features from openclaw.json
|
|
1823
1829
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
@@ -1832,10 +1838,10 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1832
1838
|
is9Router: true,
|
|
1833
1839
|
openClawNpmSpec: OPENCLAW_NPM_SPEC,
|
|
1834
1840
|
openClawRuntimePackages: '',
|
|
1835
|
-
allSkills: [],
|
|
1836
|
-
dockerfilePlugins: [],
|
|
1837
|
-
osChoice,
|
|
1838
|
-
gatewayPort,
|
|
1841
|
+
allSkills: [],
|
|
1842
|
+
dockerfilePlugins: [],
|
|
1843
|
+
osChoice,
|
|
1844
|
+
gatewayPort,
|
|
1839
1845
|
routerPort,
|
|
1840
1846
|
singleComposeName: composeName,
|
|
1841
1847
|
singleAppContainerName: botContainer,
|
|
@@ -1971,7 +1977,7 @@ async function readJson(req) {
|
|
|
1971
1977
|
const chunks = [];
|
|
1972
1978
|
for await (const chunk of req) chunks.push(chunk);
|
|
1973
1979
|
if (!chunks.length) return {};
|
|
1974
|
-
return
|
|
1980
|
+
return parseJsonText(Buffer.concat(chunks).toString('utf8'));
|
|
1975
1981
|
}
|
|
1976
1982
|
|
|
1977
1983
|
function json(res, data, status = 200) {
|
|
@@ -2204,7 +2210,7 @@ function isRestrictedSystemDir(dirPath) {
|
|
|
2204
2210
|
]);
|
|
2205
2211
|
if (unixBlacklist.has(basename(lower))) return true;
|
|
2206
2212
|
|
|
2207
|
-
if (lower.startsWith('/
|
|
2213
|
+
if (lower.startsWith('/mnt/') || lower.startsWith('/users/') || lower === '/users') {
|
|
2208
2214
|
const realHome = resolve(getRealHomedir()).toLowerCase();
|
|
2209
2215
|
if (lower !== realHome && !lower.startsWith(realHome + '/')) {
|
|
2210
2216
|
const cwd = resolve(process.cwd()).toLowerCase();
|
|
@@ -2318,32 +2324,32 @@ async function discoverProjects(rootProjectDir) {
|
|
|
2318
2324
|
return state.projects.slice(0, 20);
|
|
2319
2325
|
}
|
|
2320
2326
|
|
|
2321
|
-
async function resolveProjectDir(rootProjectDir, body = {}) {
|
|
2322
|
-
if (body.projectDir && existsSync(join(resolve(String(body.projectDir)), '.openclaw', 'openclaw.json'))) {
|
|
2323
|
-
state.projectDir = resolve(String(body.projectDir));
|
|
2324
|
-
await syncRuntimeState(state.projectDir);
|
|
2325
|
-
return state.projectDir;
|
|
2326
|
-
}
|
|
2327
|
-
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2328
|
-
await syncRuntimeState(state.projectDir);
|
|
2329
|
-
return state.projectDir;
|
|
2330
|
-
}
|
|
2331
|
-
await loadSavedState(rootProjectDir);
|
|
2332
|
-
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2333
|
-
await syncRuntimeState(state.projectDir);
|
|
2334
|
-
return state.projectDir;
|
|
2335
|
-
}
|
|
2336
|
-
const envProjectDir = process.env.OPENCLAW_PROJECT_DIR || (process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '');
|
|
2337
|
-
if (envProjectDir && existsSync(join(resolve(String(envProjectDir)), '.openclaw', 'openclaw.json'))) {
|
|
2338
|
-
state.projectDir = resolve(String(envProjectDir));
|
|
2339
|
-
await syncRuntimeState(state.projectDir);
|
|
2340
|
-
return state.projectDir;
|
|
2341
|
-
}
|
|
2342
|
-
const found = await findLatestProject(rootProjectDir);
|
|
2343
|
-
if (found) {
|
|
2344
|
-
await syncRuntimeState(found);
|
|
2345
|
-
await saveState(rootProjectDir);
|
|
2346
|
-
}
|
|
2327
|
+
async function resolveProjectDir(rootProjectDir, body = {}) {
|
|
2328
|
+
if (body.projectDir && existsSync(join(resolve(String(body.projectDir)), '.openclaw', 'openclaw.json'))) {
|
|
2329
|
+
state.projectDir = resolve(String(body.projectDir));
|
|
2330
|
+
await syncRuntimeState(state.projectDir);
|
|
2331
|
+
return state.projectDir;
|
|
2332
|
+
}
|
|
2333
|
+
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2334
|
+
await syncRuntimeState(state.projectDir);
|
|
2335
|
+
return state.projectDir;
|
|
2336
|
+
}
|
|
2337
|
+
await loadSavedState(rootProjectDir);
|
|
2338
|
+
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2339
|
+
await syncRuntimeState(state.projectDir);
|
|
2340
|
+
return state.projectDir;
|
|
2341
|
+
}
|
|
2342
|
+
const envProjectDir = process.env.OPENCLAW_PROJECT_DIR || (process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '');
|
|
2343
|
+
if (envProjectDir && existsSync(join(resolve(String(envProjectDir)), '.openclaw', 'openclaw.json'))) {
|
|
2344
|
+
state.projectDir = resolve(String(envProjectDir));
|
|
2345
|
+
await syncRuntimeState(state.projectDir);
|
|
2346
|
+
return state.projectDir;
|
|
2347
|
+
}
|
|
2348
|
+
const found = await findLatestProject(rootProjectDir);
|
|
2349
|
+
if (found) {
|
|
2350
|
+
await syncRuntimeState(found);
|
|
2351
|
+
await saveState(rootProjectDir);
|
|
2352
|
+
}
|
|
2347
2353
|
return state.projectDir;
|
|
2348
2354
|
}
|
|
2349
2355
|
|
|
@@ -2485,41 +2491,7 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2485
2491
|
|
|
2486
2492
|
const k = `${kind}:${id}`;
|
|
2487
2493
|
|
|
2488
|
-
if (kind === 'skill' && id === 'browser') {
|
|
2489
|
-
delete cfg.browser;
|
|
2490
|
-
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2491
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2492
|
-
const aliases = ['browser-automation', 'openclaw-browser-automation'];
|
|
2493
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2494
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2495
|
-
cfg.plugins.entries[existingKey].enabled = !!enabled;
|
|
2496
|
-
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2497
|
-
if (enabled) {
|
|
2498
|
-
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2499
|
-
} else {
|
|
2500
|
-
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
|
|
2501
|
-
for (const a of cfg.agents.list) {
|
|
2502
|
-
const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
|
|
2503
|
-
if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
|
|
2504
|
-
const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
|
|
2505
|
-
if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
|
|
2506
|
-
const rel = workspaceRelForAgent(a, cfg, projectDir);
|
|
2507
|
-
await fsp.rm(join(projectDir, '.openclaw', rel, 'plugin-skills', 'browser-automation'), { recursive: true, force: true }).catch(() => {});
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
// Write cfgPath early so syncDockerInfra reads the updated openclaw.json
|
|
2512
|
-
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2513
2494
|
|
|
2514
|
-
// Force Docker Infrastructure sync and container recreation
|
|
2515
|
-
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2516
|
-
if (hasDocker) {
|
|
2517
|
-
sendLog(`[docker] Browser skill toggled to ${enabled}. Regenerating Dockerfiles...`);
|
|
2518
|
-
await syncDockerInfra(projectDir, true).catch((err) => sendLog(`[docker] Warning: Failed to sync docker infra: ${err.message}`));
|
|
2519
|
-
sendLog(`[docker] Rebuilding and recreating containers...`);
|
|
2520
|
-
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
2495
|
|
|
2524
2496
|
if (kind === 'skill' && id === 'cron') {
|
|
2525
2497
|
cfg.skills = cfg.skills || { entries: {} };
|
|
@@ -2583,8 +2555,18 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2583
2555
|
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2584
2556
|
if (enabled) {
|
|
2585
2557
|
if (!cfg.plugins.allow.includes('duckduckgo')) cfg.plugins.allow.push('duckduckgo');
|
|
2558
|
+
cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
|
|
2559
|
+
cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:web']));
|
|
2586
2560
|
} else {
|
|
2587
2561
|
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== 'duckduckgo');
|
|
2562
|
+
const aliases = ['browser-automation', 'openclaw-browser-automation'];
|
|
2563
|
+
const isBrowserEnabled = aliases.some((a) => cfg.plugins?.entries?.[a]?.enabled);
|
|
2564
|
+
if (!isBrowserEnabled) {
|
|
2565
|
+
if (cfg.tools?.alsoAllow) {
|
|
2566
|
+
cfg.tools.alsoAllow = cfg.tools.alsoAllow.filter((x) => x !== 'group:web');
|
|
2567
|
+
if (cfg.tools.alsoAllow.length === 0) delete cfg.tools.alsoAllow;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2588
2570
|
}
|
|
2589
2571
|
|
|
2590
2572
|
// Write cfgPath early so recreation reads updated openclaw.json
|
|
@@ -2653,6 +2635,23 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2653
2635
|
}
|
|
2654
2636
|
}
|
|
2655
2637
|
|
|
2638
|
+
if (kind === 'skill' && id === 'learning-memory') {
|
|
2639
|
+
cfg.skills = cfg.skills || { entries: {} };
|
|
2640
|
+
cfg.skills.entries = cfg.skills.entries || {};
|
|
2641
|
+
cfg.skills.entries['learning-memory'] = cfg.skills.entries['learning-memory'] || {};
|
|
2642
|
+
cfg.skills.entries['learning-memory'].enabled = !!enabled;
|
|
2643
|
+
|
|
2644
|
+
// Write cfgPath early so recreation reads updated openclaw.json
|
|
2645
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2646
|
+
|
|
2647
|
+
// Recreate container to apply updated openclaw.json
|
|
2648
|
+
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2649
|
+
if (hasDocker) {
|
|
2650
|
+
sendLog(`[docker] Learning Memory skill toggled to ${enabled}. Recreating containers...`);
|
|
2651
|
+
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2656
2655
|
if (kind === 'plugin') {
|
|
2657
2656
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2658
2657
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
@@ -2666,29 +2665,77 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2666
2665
|
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2667
2666
|
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2668
2667
|
cfg.plugins.entries[existingKey].enabled = !!enabled;
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2668
|
+
|
|
2669
|
+
if (enabled) {
|
|
2670
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2671
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2672
|
+
}
|
|
2673
|
+
// Only add the canonical config key to allow list (not all aliases)
|
|
2674
|
+
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2675
|
+
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2676
|
+
|
|
2677
|
+
// Auto-expose zalo-mod dashboard port in docker-compose.yml when enabling
|
|
2678
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2679
|
+
const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
|
|
2680
|
+
if (existsSync(composeFile)) {
|
|
2681
|
+
try {
|
|
2682
|
+
let composeContent = await fsp.readFile(composeFile, 'utf8');
|
|
2683
|
+
const dashPort = cfg.plugins.entries[existingKey].config?.dashboardPort;
|
|
2684
|
+
if (dashPort && !composeContent.includes(`:${dashPort}`)) {
|
|
2685
|
+
const gwPortStr = String(Number(cfg.gateway?.port) || state.gatewayPort || 18789);
|
|
2686
|
+
composeContent = composeContent.replace(
|
|
2687
|
+
new RegExp(`^(\\s*-\\s*"(?:\\d+:)?${gwPortStr}(?::${gwPortStr})?"\\s*)$`, 'm'),
|
|
2688
|
+
`$1\n - "127.0.0.1:${dashPort}:${dashPort}" # zalo-mod dashboard`
|
|
2689
|
+
);
|
|
2690
|
+
await fsp.writeFile(composeFile, composeContent, 'utf8');
|
|
2691
|
+
sendLog(`[plugin] Added dashboard port ${dashPort} to docker-compose.yml`);
|
|
2692
|
+
}
|
|
2693
|
+
} catch (e) { sendLog(`[plugin] Warning: could not add dashboard port: ${e.message}`); }
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2698
|
+
cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
|
|
2699
|
+
cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:web']));
|
|
2700
|
+
|
|
2701
|
+
// Force Docker sync and recreate container to include chrome/playwright dependencies
|
|
2702
|
+
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2703
|
+
if (hasDocker) {
|
|
2704
|
+
sendLog(`[docker] Browser plugin enabled. Regenerating Dockerfiles...`);
|
|
2705
|
+
await syncDockerInfra(projectDir, true).catch((err) => sendLog(`[docker] Warning: Failed to sync docker infra: ${err.message}`));
|
|
2706
|
+
sendLog(`[docker] Rebuilding and recreating containers...`);
|
|
2707
|
+
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
} else {
|
|
2711
|
+
if (Array.isArray(cfg.plugins.allow)) {
|
|
2712
|
+
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
|
|
2713
|
+
}
|
|
2714
|
+
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2715
|
+
const isWebSearchEnabled = !!cfg.plugins?.entries?.['duckduckgo']?.enabled;
|
|
2716
|
+
if (!isWebSearchEnabled) {
|
|
2717
|
+
if (cfg.tools?.alsoAllow) {
|
|
2718
|
+
cfg.tools.alsoAllow = cfg.tools.alsoAllow.filter((x) => x !== 'group:web');
|
|
2719
|
+
if (cfg.tools.alsoAllow.length === 0) delete cfg.tools.alsoAllow;
|
|
2690
2720
|
}
|
|
2691
|
-
}
|
|
2721
|
+
}
|
|
2722
|
+
for (const a of cfg.agents.list) {
|
|
2723
|
+
const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
|
|
2724
|
+
if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
|
|
2725
|
+
const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
|
|
2726
|
+
if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
|
|
2727
|
+
const rel = workspaceRelForAgent(a, cfg, projectDir);
|
|
2728
|
+
await fsp.rm(join(projectDir, '.openclaw', rel, 'plugin-skills', 'browser-automation'), { recursive: true, force: true }).catch(() => {});
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Force Docker sync and recreate container to clean up browser dependencies/ports
|
|
2732
|
+
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2733
|
+
if (hasDocker) {
|
|
2734
|
+
sendLog(`[docker] Browser plugin disabled. Regenerating Dockerfiles...`);
|
|
2735
|
+
await syncDockerInfra(projectDir, true).catch((err) => sendLog(`[docker] Warning: Failed to sync docker infra: ${err.message}`));
|
|
2736
|
+
sendLog(`[docker] Rebuilding and recreating containers...`);
|
|
2737
|
+
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
|
|
2738
|
+
}
|
|
2692
2739
|
}
|
|
2693
2740
|
}
|
|
2694
2741
|
}
|
|
@@ -2702,6 +2749,7 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2702
2749
|
const skillSlugMap = {
|
|
2703
2750
|
'image-gen': 'infographic-generator',
|
|
2704
2751
|
'sticker-mention': 'zalo-sticker-mention',
|
|
2752
|
+
'learning-memory': 'learning-memory',
|
|
2705
2753
|
};
|
|
2706
2754
|
const slug = skillSlugMap[id] || id;
|
|
2707
2755
|
|
|
@@ -2797,17 +2845,19 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2797
2845
|
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2798
2846
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2799
2847
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2800
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2801
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2802
|
-
cfg.plugins.entries[existingKey].enabled = true;
|
|
2803
|
-
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2804
|
-
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
2805
|
-
hostOs: await resolveProjectHostOs(projectDir),
|
|
2806
|
-
});
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2848
|
+
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2849
|
+
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2850
|
+
cfg.plugins.entries[existingKey].enabled = true;
|
|
2851
|
+
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2852
|
+
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
2853
|
+
hostOs: await resolveProjectHostOs(projectDir),
|
|
2854
|
+
});
|
|
2855
|
+
cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
|
|
2856
|
+
cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:web']));
|
|
2857
|
+
}
|
|
2858
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2859
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2860
|
+
}
|
|
2811
2861
|
// Only add the canonical config key to allow list (not all aliases)
|
|
2812
2862
|
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2813
2863
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
@@ -2835,15 +2885,15 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2835
2885
|
}
|
|
2836
2886
|
}
|
|
2837
2887
|
|
|
2838
|
-
// Browser-automation plugin needs Docker rebuild for Playwright/Chromium deps
|
|
2839
|
-
const isBrowserPlugin = id === 'openclaw-browser-automation' || id === 'browser-automation';
|
|
2840
|
-
if (isBrowserPlugin && composeDir) {
|
|
2841
|
-
await patchBrowserAutomationHostPreference(projectDir, aliases, sendLog);
|
|
2842
|
-
sendLog(`[plugin] Browser plugin requires Docker rebuild for Playwright/Chromium...`);
|
|
2843
|
-
const svcName = getBotServiceName(projectDir);
|
|
2844
|
-
await run('docker', ['compose', '-f', join(composeDir, 'docker-compose.yml'), 'up', '-d', '--build', '--force-recreate', svcName], { shell: false }).catch((err) => {
|
|
2845
|
-
sendLog(`[plugin] Docker rebuild failed: ${err.message}. Falling back to restart...`);
|
|
2846
|
-
return run('docker', ['restart', botContainer], { shell: false });
|
|
2888
|
+
// Browser-automation plugin needs Docker rebuild for Playwright/Chromium deps
|
|
2889
|
+
const isBrowserPlugin = id === 'openclaw-browser-automation' || id === 'browser-automation';
|
|
2890
|
+
if (isBrowserPlugin && composeDir) {
|
|
2891
|
+
await patchBrowserAutomationHostPreference(projectDir, aliases, sendLog);
|
|
2892
|
+
sendLog(`[plugin] Browser plugin requires Docker rebuild for Playwright/Chromium...`);
|
|
2893
|
+
const svcName = getBotServiceName(projectDir);
|
|
2894
|
+
await run('docker', ['compose', '-f', join(composeDir, 'docker-compose.yml'), 'up', '-d', '--build', '--force-recreate', svcName], { shell: false }).catch((err) => {
|
|
2895
|
+
sendLog(`[plugin] Docker rebuild failed: ${err.message}. Falling back to restart...`);
|
|
2896
|
+
return run('docker', ['restart', botContainer], { shell: false });
|
|
2847
2897
|
});
|
|
2848
2898
|
} else if (isZaloMod && composeDir) {
|
|
2849
2899
|
// Use docker compose up to apply new port mappings from docker-compose.yml
|
|
@@ -2879,37 +2929,39 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2879
2929
|
|
|
2880
2930
|
// Automatically enable it in config after install
|
|
2881
2931
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
2882
|
-
if (existsSync(cfgPath)) {
|
|
2883
|
-
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2884
|
-
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2885
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2886
|
-
const pluginAliasMap = {
|
|
2887
|
-
'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
|
|
2888
|
-
'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
|
|
2889
|
-
'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
|
|
2890
|
-
'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
|
|
2891
|
-
};
|
|
2892
|
-
const aliases = pluginAliasMap[id] || [id];
|
|
2893
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2894
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2895
|
-
cfg.plugins.entries[existingKey].enabled = true;
|
|
2896
|
-
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2897
|
-
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
2898
|
-
hostOs: await resolveProjectHostOs(projectDir),
|
|
2899
|
-
});
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2932
|
+
if (existsSync(cfgPath)) {
|
|
2933
|
+
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2934
|
+
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2935
|
+
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2936
|
+
const pluginAliasMap = {
|
|
2937
|
+
'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
|
|
2938
|
+
'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
|
|
2939
|
+
'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
|
|
2940
|
+
'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
|
|
2941
|
+
};
|
|
2942
|
+
const aliases = pluginAliasMap[id] || [id];
|
|
2943
|
+
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2944
|
+
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2945
|
+
cfg.plugins.entries[existingKey].enabled = true;
|
|
2946
|
+
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2947
|
+
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
2948
|
+
hostOs: await resolveProjectHostOs(projectDir),
|
|
2949
|
+
});
|
|
2950
|
+
cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
|
|
2951
|
+
cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:web']));
|
|
2952
|
+
}
|
|
2953
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2954
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2955
|
+
}
|
|
2956
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
if (id === 'openclaw-browser-automation' || id === 'browser-automation') {
|
|
2960
|
+
const aliases = ['browser-automation', 'openclaw-browser-automation'];
|
|
2961
|
+
await patchBrowserAutomationHostPreference(projectDir, aliases, sendLog);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2913
2965
|
return { ok: true };
|
|
2914
2966
|
}
|
|
2915
2967
|
|
|
@@ -2980,14 +3032,14 @@ async function getInstalledSkillVersion(projectDir, agentId, skillFolder, cfg =
|
|
|
2980
3032
|
|
|
2981
3033
|
async function getFeatureFlags(projectDir, agentId = '') {
|
|
2982
3034
|
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
2983
|
-
const cfg = existsSync(cfgPath) ? ensureConfigShape(
|
|
3035
|
+
const cfg = existsSync(cfgPath) ? ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'), {})) : {};
|
|
2984
3036
|
const aid = agentId || cfg.agents?.list?.[0]?.id || 'bot';
|
|
2985
3037
|
const browserOn = !!cfg.browser?.enabled;
|
|
2986
3038
|
const cronOn = !!cfg.skills?.entries?.['cronjob']?.enabled || !!cfg.skills?.entries?.['cron']?.enabled || !!(cfg.tools?.alsoAllow || []).includes('group:automation');
|
|
2987
3039
|
const fresh = cfg;
|
|
2988
3040
|
const freshSaved = {};
|
|
2989
3041
|
const installsPath = join(projectDir || '', '.openclaw', 'plugins', 'installs.json');
|
|
2990
|
-
const installs = existsSync(installsPath) ?
|
|
3042
|
+
const installs = existsSync(installsPath) ? parseJsonText(await fsp.readFile(installsPath, 'utf8').catch(() => '{}'), {}) : {};
|
|
2991
3043
|
const installRecords = installs.installRecords || {};
|
|
2992
3044
|
const installedKeys = new Set(Object.keys(installRecords).map((k) => String(k || '').toLowerCase()));
|
|
2993
3045
|
const installedSpecs = new Set(Object.values(installRecords).flatMap((r) => {
|
|
@@ -3013,6 +3065,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3013
3065
|
const imageGenOn = !!cfg.skills?.entries?.['image-gen']?.enabled;
|
|
3014
3066
|
const webSearchOn = isEnabled(['duckduckgo']);
|
|
3015
3067
|
const stickerMentionOn = !!cfg.skills?.entries?.['sticker-mention']?.enabled;
|
|
3068
|
+
const learningMemoryOn = !!cfg.skills?.entries?.['learning-memory']?.enabled;
|
|
3016
3069
|
const aliases = {
|
|
3017
3070
|
browser: ['openclaw-browser-automation', 'browser-automation'],
|
|
3018
3071
|
zalo: ['openclaw-zalo-mod', 'zalo-mod'],
|
|
@@ -3025,6 +3078,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3025
3078
|
'skill:image-gen': imageGenOn,
|
|
3026
3079
|
'skill:web-search': webSearchOn,
|
|
3027
3080
|
'skill:sticker-mention': stickerMentionOn,
|
|
3081
|
+
'skill:learning-memory': learningMemoryOn,
|
|
3028
3082
|
'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
|
|
3029
3083
|
'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
|
|
3030
3084
|
'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
|
|
@@ -3038,6 +3092,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3038
3092
|
const installed = {
|
|
3039
3093
|
'skill:image-gen': isSkillFolderExists(projectDir, aid, 'infographic-generator', cfg),
|
|
3040
3094
|
'skill:sticker-mention': isSkillFolderExists(projectDir, aid, 'zalo-sticker-mention', cfg),
|
|
3095
|
+
'skill:learning-memory': isSkillFolderExists(projectDir, aid, 'learning-memory', cfg),
|
|
3041
3096
|
'plugin:openclaw-browser-automation': isActuallyInstalled(aliases.browser),
|
|
3042
3097
|
'plugin:openclaw-zalo-mod': isActuallyInstalled(aliases.zalo),
|
|
3043
3098
|
'plugin:openclaw-facebook-crawler': isActuallyInstalled(aliases.crawler),
|
|
@@ -3046,6 +3101,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3046
3101
|
const versions = {
|
|
3047
3102
|
'skill:image-gen': await getInstalledSkillVersion(projectDir, aid, 'infographic-generator', cfg),
|
|
3048
3103
|
'skill:sticker-mention': await getInstalledSkillVersion(projectDir, aid, 'zalo-sticker-mention', cfg),
|
|
3104
|
+
'skill:learning-memory': await getInstalledSkillVersion(projectDir, aid, 'learning-memory', cfg),
|
|
3049
3105
|
'plugin:openclaw-browser-automation': await getInstalledPluginVersion(projectDir, aliases.browser),
|
|
3050
3106
|
'plugin:openclaw-zalo-mod': await getInstalledPluginVersion(projectDir, aliases.zalo),
|
|
3051
3107
|
'plugin:openclaw-facebook-crawler': await getInstalledPluginVersion(projectDir, aliases.crawler),
|
|
@@ -3183,10 +3239,10 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3183
3239
|
const body = await readJson(req);
|
|
3184
3240
|
return json(res, await connectPickedProject(body.projectName, rootProjectDir));
|
|
3185
3241
|
}
|
|
3186
|
-
if (url.pathname === '/api/bot/status' && req.method === 'GET') {
|
|
3187
|
-
await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3188
|
-
return json(res, await buildBotStatus());
|
|
3189
|
-
}
|
|
3242
|
+
if (url.pathname === '/api/bot/status' && req.method === 'GET') {
|
|
3243
|
+
await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3244
|
+
return json(res, await buildBotStatus());
|
|
3245
|
+
}
|
|
3190
3246
|
if (url.pathname === '/api/bot/credentials' && req.method === 'PUT') {
|
|
3191
3247
|
const body = await readJson(req);
|
|
3192
3248
|
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
@@ -3313,36 +3369,36 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3313
3369
|
});
|
|
3314
3370
|
return json(res, { ok: true, message: 'Zalo login initiated. QR will appear in UI.' });
|
|
3315
3371
|
}
|
|
3316
|
-
if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
|
|
3317
|
-
const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
|
|
3318
|
-
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3319
|
-
const result = await deleteBotInProject(projectDir, agentId);
|
|
3320
|
-
sendLog(`? Bot deleted: ${agentId}`);
|
|
3321
|
-
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
|
|
3322
|
-
return json(res, result);
|
|
3323
|
-
}
|
|
3324
|
-
if (url.pathname === '/api/bot/files' && req.method === 'GET') {
|
|
3325
|
-
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3326
|
-
if (!projectDir) return json(res, { files: [] });
|
|
3327
|
-
const agentId = url.searchParams.get('agentId') || '';
|
|
3328
|
-
return json(res, { files: await listMarkdownFiles(projectDir, agentId).catch(() => []) });
|
|
3329
|
-
}
|
|
3330
|
-
if (url.pathname.startsWith('/api/bot/files/')) {
|
|
3331
|
-
const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
|
|
3332
|
-
if (req.method === 'GET') {
|
|
3333
|
-
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3334
|
-
const file = safeJoin(projectDir, name);
|
|
3335
|
-
return json(res, { name, content: await fsp.readFile(file, 'utf8') });
|
|
3336
|
-
}
|
|
3337
|
-
if (req.method === 'PUT') {
|
|
3338
|
-
if (!name.endsWith('.md')) throw httpError(400, 'Only markdown files (.md) can be modified');
|
|
3339
|
-
const body = await readJson(req);
|
|
3340
|
-
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3341
|
-
const file = safeJoin(projectDir, name);
|
|
3342
|
-
await fsp.writeFile(file, String(body.content || ''), 'utf8');
|
|
3343
|
-
return json(res, { ok: true });
|
|
3344
|
-
}
|
|
3345
|
-
}
|
|
3372
|
+
if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
|
|
3373
|
+
const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
|
|
3374
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3375
|
+
const result = await deleteBotInProject(projectDir, agentId);
|
|
3376
|
+
sendLog(`? Bot deleted: ${agentId}`);
|
|
3377
|
+
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
|
|
3378
|
+
return json(res, result);
|
|
3379
|
+
}
|
|
3380
|
+
if (url.pathname === '/api/bot/files' && req.method === 'GET') {
|
|
3381
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3382
|
+
if (!projectDir) return json(res, { files: [] });
|
|
3383
|
+
const agentId = url.searchParams.get('agentId') || '';
|
|
3384
|
+
return json(res, { files: await listMarkdownFiles(projectDir, agentId).catch(() => []) });
|
|
3385
|
+
}
|
|
3386
|
+
if (url.pathname.startsWith('/api/bot/files/')) {
|
|
3387
|
+
const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
|
|
3388
|
+
if (req.method === 'GET') {
|
|
3389
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3390
|
+
const file = safeJoin(projectDir, name);
|
|
3391
|
+
return json(res, { name, content: await fsp.readFile(file, 'utf8') });
|
|
3392
|
+
}
|
|
3393
|
+
if (req.method === 'PUT') {
|
|
3394
|
+
if (!name.endsWith('.md')) throw httpError(400, 'Only markdown files (.md) can be modified');
|
|
3395
|
+
const body = await readJson(req);
|
|
3396
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3397
|
+
const file = safeJoin(projectDir, name);
|
|
3398
|
+
await fsp.writeFile(file, String(body.content || ''), 'utf8');
|
|
3399
|
+
return json(res, { ok: true });
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3346
3402
|
if (url.pathname === '/api/catalog' && req.method === 'GET') return json(res, {
|
|
3347
3403
|
skills: [
|
|
3348
3404
|
{ name: 'Browser', slug: 'browser' },
|
|
@@ -3357,21 +3413,21 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3357
3413
|
{ name: 'openclaw-n8n-facebook-poster', package: 'openclaw-n8n-facebook-poster' },
|
|
3358
3414
|
]
|
|
3359
3415
|
});
|
|
3360
|
-
if (url.pathname === '/api/features' && req.method === 'GET') {
|
|
3361
|
-
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3362
|
-
if (!projectDir) return json(res, { flags: {}, installed: {}, versions: {} });
|
|
3363
|
-
return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
|
|
3364
|
-
}
|
|
3365
|
-
if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
|
|
3366
|
-
const body = await readJson(req);
|
|
3367
|
-
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3368
|
-
return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
|
|
3369
|
-
}
|
|
3370
|
-
if (url.pathname === '/api/features/install' && req.method === 'POST') {
|
|
3371
|
-
const body = await readJson(req);
|
|
3372
|
-
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3373
|
-
return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
|
|
3374
|
-
}
|
|
3416
|
+
if (url.pathname === '/api/features' && req.method === 'GET') {
|
|
3417
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3418
|
+
if (!projectDir) return json(res, { flags: {}, installed: {}, versions: {} });
|
|
3419
|
+
return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
|
|
3420
|
+
}
|
|
3421
|
+
if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
|
|
3422
|
+
const body = await readJson(req);
|
|
3423
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3424
|
+
return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
|
|
3425
|
+
}
|
|
3426
|
+
if (url.pathname === '/api/features/install' && req.method === 'POST') {
|
|
3427
|
+
const body = await readJson(req);
|
|
3428
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3429
|
+
return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
|
|
3430
|
+
}
|
|
3375
3431
|
if (await serveStatic(req, res)) return;
|
|
3376
3432
|
json(res, { error: 'Not found' }, 404);
|
|
3377
3433
|
} catch (err) {
|