create-openclaw-bot 5.8.21 → 5.8.24
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 +23 -18
- package/README.vi.md +22 -17
- package/dist/cli.js +36 -8
- package/dist/server/local-server.js +511 -461
- 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();
|
|
@@ -609,21 +609,21 @@ async function readEnvFile(file) {
|
|
|
609
609
|
return existsSync(file) ? readEnvText(await fsp.readFile(file, 'utf8').catch(() => '')) : {};
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
function openclawProjectEnv(projectDir) {
|
|
613
|
-
return {
|
|
614
|
-
...process.env,
|
|
615
|
-
OPENCLAW_HOME: join(projectDir, '.openclaw'),
|
|
616
|
-
OPENCLAW_STATE_DIR: join(projectDir, '.openclaw'),
|
|
617
|
-
};
|
|
618
|
-
}
|
|
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
|
-
|
|
626
|
-
async function runOpenclawJson(projectDir, args = [], timeout = 12000) {
|
|
612
|
+
function openclawProjectEnv(projectDir) {
|
|
613
|
+
return {
|
|
614
|
+
...process.env,
|
|
615
|
+
OPENCLAW_HOME: join(projectDir, '.openclaw'),
|
|
616
|
+
OPENCLAW_STATE_DIR: join(projectDir, '.openclaw'),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
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
|
+
|
|
626
|
+
async function runOpenclawJson(projectDir, args = [], timeout = 12000) {
|
|
627
627
|
const out = await runCapture('openclaw', args, {
|
|
628
628
|
cwd: projectDir,
|
|
629
629
|
env: openclawProjectEnv(projectDir),
|
|
@@ -632,8 +632,8 @@ async function runOpenclawJson(projectDir, args = [], timeout = 12000) {
|
|
|
632
632
|
});
|
|
633
633
|
if (out.code !== 0) throw new Error((out.stderr || out.stdout || `openclaw ${args.join(' ')} failed`).trim());
|
|
634
634
|
const text = String(out.stdout || '').trim();
|
|
635
|
-
return text ? parseJsonText(text) : null;
|
|
636
|
-
}
|
|
635
|
+
return text ? parseJsonText(text) : null;
|
|
636
|
+
}
|
|
637
637
|
|
|
638
638
|
async function readComposeText(projectDir) {
|
|
639
639
|
const p = join(projectDir || '', 'docker', 'openclaw', 'docker-compose.yml');
|
|
@@ -678,9 +678,9 @@ function parseBaseUrlPort(baseUrl = '') {
|
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
|
|
681
|
-
async function detectRuntime(projectDir) {
|
|
682
|
-
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
683
|
-
const cfg = existsSync(cfgPath) ? parseJsonText(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'), {}) : {};
|
|
681
|
+
async function detectRuntime(projectDir) {
|
|
682
|
+
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
683
|
+
const cfg = existsSync(cfgPath) ? parseJsonText(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'), {}) : {};
|
|
684
684
|
let cliGatewayStatus = null;
|
|
685
685
|
let cliGatewayPort = 0;
|
|
686
686
|
let cliRouterPort = 0;
|
|
@@ -1045,11 +1045,11 @@ async function resolveProject9RouterApiKey(projectDir, cfg = null) {
|
|
|
1045
1045
|
return '';
|
|
1046
1046
|
}
|
|
1047
1047
|
|
|
1048
|
-
async function applyResolved9RouterApiKey(projectDir, cfg = null) {
|
|
1049
|
-
if (!projectDir) return '';
|
|
1050
|
-
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
1051
|
-
if (!existsSync(cfgPath)) return '';
|
|
1052
|
-
const current = cfg || ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8')));
|
|
1048
|
+
async function applyResolved9RouterApiKey(projectDir, cfg = null) {
|
|
1049
|
+
if (!projectDir) return '';
|
|
1050
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
1051
|
+
if (!existsSync(cfgPath)) return '';
|
|
1052
|
+
const current = cfg || ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8')));
|
|
1053
1053
|
const apiKey = await resolveProject9RouterApiKey(projectDir, current);
|
|
1054
1054
|
if (!apiKey) return '';
|
|
1055
1055
|
current.models = current.models || { mode: 'merge', providers: {} };
|
|
@@ -1062,21 +1062,21 @@ async function applyResolved9RouterApiKey(projectDir, cfg = null) {
|
|
|
1062
1062
|
return apiKey;
|
|
1063
1063
|
}
|
|
1064
1064
|
|
|
1065
|
-
async function readBotCredentials(projectDir) {
|
|
1066
|
-
const found = readProjectConfig(projectDir);
|
|
1067
|
-
if (!found) return { openclawToken: '', nineRouterApiKey: '' };
|
|
1068
|
-
const cfg = ensureConfigShape(parseJsonText(await fsp.readFile(found.cfgPath, 'utf8')));
|
|
1065
|
+
async function readBotCredentials(projectDir) {
|
|
1066
|
+
const found = readProjectConfig(projectDir);
|
|
1067
|
+
if (!found) return { openclawToken: '', nineRouterApiKey: '' };
|
|
1068
|
+
const cfg = ensureConfigShape(parseJsonText(await fsp.readFile(found.cfgPath, 'utf8')));
|
|
1069
1069
|
return {
|
|
1070
1070
|
openclawToken: cfg.gateway?.auth?.token || '',
|
|
1071
1071
|
nineRouterApiKey: await resolveProject9RouterApiKey(projectDir, cfg),
|
|
1072
1072
|
};
|
|
1073
1073
|
}
|
|
1074
1074
|
|
|
1075
|
-
async function updateBotCredentials(projectDir, body = {}) {
|
|
1076
|
-
const found = readProjectConfig(projectDir);
|
|
1077
|
-
if (!found) throw httpError(400, 'Install project not found');
|
|
1078
|
-
const raw = await fsp.readFile(found.cfgPath, 'utf8');
|
|
1079
|
-
const cfg = ensureConfigShape(parseJsonText(raw));
|
|
1075
|
+
async function updateBotCredentials(projectDir, body = {}) {
|
|
1076
|
+
const found = readProjectConfig(projectDir);
|
|
1077
|
+
if (!found) throw httpError(400, 'Install project not found');
|
|
1078
|
+
const raw = await fsp.readFile(found.cfgPath, 'utf8');
|
|
1079
|
+
const cfg = ensureConfigShape(parseJsonText(raw));
|
|
1080
1080
|
const nineRouterApiKey = String(body.nineRouterApiKey || '').trim();
|
|
1081
1081
|
if (Object.prototype.hasOwnProperty.call(body, 'nineRouterApiKey')) {
|
|
1082
1082
|
cfg.models = cfg.models || { mode: 'merge', providers: {} };
|
|
@@ -1144,11 +1144,11 @@ function mapAgentChannels(agent, cfg) {
|
|
|
1144
1144
|
return enabled.length === 1 ? enabled : [mapAgentChannel(agent, cfg)];
|
|
1145
1145
|
}
|
|
1146
1146
|
|
|
1147
|
-
async function listConfiguredBots(projectDir) {
|
|
1148
|
-
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
1149
|
-
if (!projectDir || !existsSync(cfgPath)) return [];
|
|
1150
|
-
const raw = await fsp.readFile(cfgPath, 'utf8');
|
|
1151
|
-
const cfg = ensureConfigShape(parseJsonText(raw));
|
|
1147
|
+
async function listConfiguredBots(projectDir) {
|
|
1148
|
+
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
1149
|
+
if (!projectDir || !existsSync(cfgPath)) return [];
|
|
1150
|
+
const raw = await fsp.readFile(cfgPath, 'utf8');
|
|
1151
|
+
const cfg = ensureConfigShape(parseJsonText(raw));
|
|
1152
1152
|
const normalized = JSON.stringify(cfg, null, 2) + '\n';
|
|
1153
1153
|
if (normalized !== raw) await fsp.writeFile(cfgPath, normalized, 'utf8');
|
|
1154
1154
|
const rows = await Promise.all(cfg.agents.list.map(async (agent) => {
|
|
@@ -1176,14 +1176,14 @@ async function rmInside(root, target) {
|
|
|
1176
1176
|
await fsp.rm(targetFull, { recursive: true, force: true }).catch(() => {});
|
|
1177
1177
|
}
|
|
1178
1178
|
|
|
1179
|
-
async function deleteBotInProject(projectDir, agentId) {
|
|
1179
|
+
async function deleteBotInProject(projectDir, agentId) {
|
|
1180
1180
|
if (!projectDir) throw httpError(400, 'Install project not found');
|
|
1181
1181
|
const cleanId = slugify(agentId, '');
|
|
1182
1182
|
if (!cleanId || cleanId !== agentId) throw httpError(400, 'Invalid bot id');
|
|
1183
1183
|
const openclawHome = join(projectDir, '.openclaw');
|
|
1184
1184
|
const cfgPath = join(openclawHome, 'openclaw.json');
|
|
1185
1185
|
if (!existsSync(cfgPath)) throw httpError(404, 'openclaw.json not found');
|
|
1186
|
-
const cfg = ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8')));
|
|
1186
|
+
const cfg = ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8')));
|
|
1187
1187
|
const agent = cfg.agents.list.find((a) => a.id === agentId);
|
|
1188
1188
|
if (!agent) throw httpError(404, 'Bot not found');
|
|
1189
1189
|
|
|
@@ -1821,9 +1821,9 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1821
1821
|
const botContainer = parseComposeServiceContainerName(compose, 'ai-bot') || `openclaw-${slugify(basename(projectDir))}`;
|
|
1822
1822
|
const routerContainer = parseComposeServiceContainerName(compose, '9router') || `9router-${slugify(basename(projectDir))}`;
|
|
1823
1823
|
const composeName = (compose.match(/^name:\s*(\S+)/m) || [])[1] || `oc-${slugify(basename(projectDir))}`;
|
|
1824
|
-
const gatewayPort = state.gatewayPort || 18789;
|
|
1825
|
-
const routerPort = state.routerPort || 20128;
|
|
1826
|
-
const osChoice = await resolveProjectHostOs(projectDir);
|
|
1824
|
+
const gatewayPort = state.gatewayPort || 18789;
|
|
1825
|
+
const routerPort = state.routerPort || 20128;
|
|
1826
|
+
const osChoice = await resolveProjectHostOs(projectDir);
|
|
1827
1827
|
|
|
1828
1828
|
// Detect features from openclaw.json
|
|
1829
1829
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
@@ -1838,10 +1838,10 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1838
1838
|
is9Router: true,
|
|
1839
1839
|
openClawNpmSpec: OPENCLAW_NPM_SPEC,
|
|
1840
1840
|
openClawRuntimePackages: '',
|
|
1841
|
-
allSkills: [],
|
|
1842
|
-
dockerfilePlugins: [],
|
|
1843
|
-
osChoice,
|
|
1844
|
-
gatewayPort,
|
|
1841
|
+
allSkills: [],
|
|
1842
|
+
dockerfilePlugins: [],
|
|
1843
|
+
osChoice,
|
|
1844
|
+
gatewayPort,
|
|
1845
1845
|
routerPort,
|
|
1846
1846
|
singleComposeName: composeName,
|
|
1847
1847
|
singleAppContainerName: botContainer,
|
|
@@ -1973,12 +1973,12 @@ async function restartDockerBotContainer(projectDir = state.projectDir) {
|
|
|
1973
1973
|
return true;
|
|
1974
1974
|
}
|
|
1975
1975
|
|
|
1976
|
-
async function readJson(req) {
|
|
1977
|
-
const chunks = [];
|
|
1978
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
1979
|
-
if (!chunks.length) return {};
|
|
1980
|
-
return parseJsonText(Buffer.concat(chunks).toString('utf8'));
|
|
1981
|
-
}
|
|
1976
|
+
async function readJson(req) {
|
|
1977
|
+
const chunks = [];
|
|
1978
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1979
|
+
if (!chunks.length) return {};
|
|
1980
|
+
return parseJsonText(Buffer.concat(chunks).toString('utf8'));
|
|
1981
|
+
}
|
|
1982
1982
|
|
|
1983
1983
|
function json(res, data, status = 200) {
|
|
1984
1984
|
const body = JSON.stringify(data, null, 2);
|
|
@@ -2210,7 +2210,7 @@ function isRestrictedSystemDir(dirPath) {
|
|
|
2210
2210
|
]);
|
|
2211
2211
|
if (unixBlacklist.has(basename(lower))) return true;
|
|
2212
2212
|
|
|
2213
|
-
if (lower.startsWith('/
|
|
2213
|
+
if (lower.startsWith('/mnt/') || lower.startsWith('/users/') || lower === '/users') {
|
|
2214
2214
|
const realHome = resolve(getRealHomedir()).toLowerCase();
|
|
2215
2215
|
if (lower !== realHome && !lower.startsWith(realHome + '/')) {
|
|
2216
2216
|
const cwd = resolve(process.cwd()).toLowerCase();
|
|
@@ -2324,32 +2324,32 @@ async function discoverProjects(rootProjectDir) {
|
|
|
2324
2324
|
return state.projects.slice(0, 20);
|
|
2325
2325
|
}
|
|
2326
2326
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
2353
2353
|
return state.projectDir;
|
|
2354
2354
|
}
|
|
2355
2355
|
|
|
@@ -2491,41 +2491,7 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2491
2491
|
|
|
2492
2492
|
const k = `${kind}:${id}`;
|
|
2493
2493
|
|
|
2494
|
-
if (kind === 'skill' && id === 'browser') {
|
|
2495
|
-
delete cfg.browser;
|
|
2496
|
-
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2497
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2498
|
-
const aliases = ['browser-automation', 'openclaw-browser-automation'];
|
|
2499
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2500
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2501
|
-
cfg.plugins.entries[existingKey].enabled = !!enabled;
|
|
2502
|
-
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2503
|
-
if (enabled) {
|
|
2504
|
-
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2505
|
-
} else {
|
|
2506
|
-
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
|
|
2507
|
-
for (const a of cfg.agents.list) {
|
|
2508
|
-
const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
|
|
2509
|
-
if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
|
|
2510
|
-
const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
|
|
2511
|
-
if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
|
|
2512
|
-
const rel = workspaceRelForAgent(a, cfg, projectDir);
|
|
2513
|
-
await fsp.rm(join(projectDir, '.openclaw', rel, 'plugin-skills', 'browser-automation'), { recursive: true, force: true }).catch(() => {});
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
// Write cfgPath early so syncDockerInfra reads the updated openclaw.json
|
|
2518
|
-
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2519
2494
|
|
|
2520
|
-
// Force Docker Infrastructure sync and container recreation
|
|
2521
|
-
const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2522
|
-
if (hasDocker) {
|
|
2523
|
-
sendLog(`[docker] Browser skill toggled to ${enabled}. Regenerating Dockerfiles...`);
|
|
2524
|
-
await syncDockerInfra(projectDir, true).catch((err) => sendLog(`[docker] Warning: Failed to sync docker infra: ${err.message}`));
|
|
2525
|
-
sendLog(`[docker] Rebuilding and recreating containers...`);
|
|
2526
|
-
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
2495
|
|
|
2530
2496
|
if (kind === 'skill' && id === 'cron') {
|
|
2531
2497
|
cfg.skills = cfg.skills || { entries: {} };
|
|
@@ -2589,8 +2555,18 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2589
2555
|
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2590
2556
|
if (enabled) {
|
|
2591
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']));
|
|
2592
2560
|
} else {
|
|
2593
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
|
+
}
|
|
2594
2570
|
}
|
|
2595
2571
|
|
|
2596
2572
|
// Write cfgPath early so recreation reads updated openclaw.json
|
|
@@ -2659,6 +2635,23 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2659
2635
|
}
|
|
2660
2636
|
}
|
|
2661
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
|
+
|
|
2662
2655
|
if (kind === 'plugin') {
|
|
2663
2656
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2664
2657
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
@@ -2672,29 +2665,77 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2672
2665
|
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2673
2666
|
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2674
2667
|
cfg.plugins.entries[existingKey].enabled = !!enabled;
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
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;
|
|
2696
2720
|
}
|
|
2697
|
-
}
|
|
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
|
+
}
|
|
2698
2739
|
}
|
|
2699
2740
|
}
|
|
2700
2741
|
}
|
|
@@ -2708,6 +2749,7 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2708
2749
|
const skillSlugMap = {
|
|
2709
2750
|
'image-gen': 'infographic-generator',
|
|
2710
2751
|
'sticker-mention': 'zalo-sticker-mention',
|
|
2752
|
+
'learning-memory': 'learning-memory',
|
|
2711
2753
|
};
|
|
2712
2754
|
const slug = skillSlugMap[id] || id;
|
|
2713
2755
|
|
|
@@ -2803,17 +2845,19 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2803
2845
|
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2804
2846
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2805
2847
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2806
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2807
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2808
|
-
cfg.plugins.entries[existingKey].enabled = true;
|
|
2809
|
-
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2810
|
-
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
2811
|
-
hostOs: await resolveProjectHostOs(projectDir),
|
|
2812
|
-
});
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
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
|
+
}
|
|
2817
2861
|
// Only add the canonical config key to allow list (not all aliases)
|
|
2818
2862
|
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2819
2863
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
@@ -2841,15 +2885,15 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2841
2885
|
}
|
|
2842
2886
|
}
|
|
2843
2887
|
|
|
2844
|
-
// Browser-automation plugin needs Docker rebuild for Playwright/Chromium deps
|
|
2845
|
-
const isBrowserPlugin = id === 'openclaw-browser-automation' || id === 'browser-automation';
|
|
2846
|
-
if (isBrowserPlugin && composeDir) {
|
|
2847
|
-
await patchBrowserAutomationHostPreference(projectDir, aliases, sendLog);
|
|
2848
|
-
sendLog(`[plugin] Browser plugin requires Docker rebuild for Playwright/Chromium...`);
|
|
2849
|
-
const svcName = getBotServiceName(projectDir);
|
|
2850
|
-
await run('docker', ['compose', '-f', join(composeDir, 'docker-compose.yml'), 'up', '-d', '--build', '--force-recreate', svcName], { shell: false }).catch((err) => {
|
|
2851
|
-
sendLog(`[plugin] Docker rebuild failed: ${err.message}. Falling back to restart...`);
|
|
2852
|
-
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 });
|
|
2853
2897
|
});
|
|
2854
2898
|
} else if (isZaloMod && composeDir) {
|
|
2855
2899
|
// Use docker compose up to apply new port mappings from docker-compose.yml
|
|
@@ -2885,37 +2929,39 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2885
2929
|
|
|
2886
2930
|
// Automatically enable it in config after install
|
|
2887
2931
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
2888
|
-
if (existsSync(cfgPath)) {
|
|
2889
|
-
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2890
|
-
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2891
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2892
|
-
const pluginAliasMap = {
|
|
2893
|
-
'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
|
|
2894
|
-
'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
|
|
2895
|
-
'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
|
|
2896
|
-
'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
|
|
2897
|
-
};
|
|
2898
|
-
const aliases = pluginAliasMap[id] || [id];
|
|
2899
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2900
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2901
|
-
cfg.plugins.entries[existingKey].enabled = true;
|
|
2902
|
-
if (existingKey === 'browser-automation' || existingKey === 'openclaw-browser-automation') {
|
|
2903
|
-
cfg.plugins.entries[existingKey].config = Object.assign({}, cfg.plugins.entries[existingKey].config, {
|
|
2904
|
-
hostOs: await resolveProjectHostOs(projectDir),
|
|
2905
|
-
});
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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
|
+
}
|
|
2919
2965
|
return { ok: true };
|
|
2920
2966
|
}
|
|
2921
2967
|
|
|
@@ -2984,16 +3030,16 @@ async function getInstalledSkillVersion(projectDir, agentId, skillFolder, cfg =
|
|
|
2984
3030
|
return '';
|
|
2985
3031
|
}
|
|
2986
3032
|
|
|
2987
|
-
async function getFeatureFlags(projectDir, agentId = '') {
|
|
2988
|
-
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
2989
|
-
const cfg = existsSync(cfgPath) ? ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'), {})) : {};
|
|
3033
|
+
async function getFeatureFlags(projectDir, agentId = '') {
|
|
3034
|
+
const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
|
|
3035
|
+
const cfg = existsSync(cfgPath) ? ensureConfigShape(parseJsonText(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'), {})) : {};
|
|
2990
3036
|
const aid = agentId || cfg.agents?.list?.[0]?.id || 'bot';
|
|
2991
3037
|
const browserOn = !!cfg.browser?.enabled;
|
|
2992
3038
|
const cronOn = !!cfg.skills?.entries?.['cronjob']?.enabled || !!cfg.skills?.entries?.['cron']?.enabled || !!(cfg.tools?.alsoAllow || []).includes('group:automation');
|
|
2993
|
-
const fresh = cfg;
|
|
2994
|
-
const freshSaved = {};
|
|
2995
|
-
const installsPath = join(projectDir || '', '.openclaw', 'plugins', 'installs.json');
|
|
2996
|
-
const installs = existsSync(installsPath) ? parseJsonText(await fsp.readFile(installsPath, 'utf8').catch(() => '{}'), {}) : {};
|
|
3039
|
+
const fresh = cfg;
|
|
3040
|
+
const freshSaved = {};
|
|
3041
|
+
const installsPath = join(projectDir || '', '.openclaw', 'plugins', 'installs.json');
|
|
3042
|
+
const installs = existsSync(installsPath) ? parseJsonText(await fsp.readFile(installsPath, 'utf8').catch(() => '{}'), {}) : {};
|
|
2997
3043
|
const installRecords = installs.installRecords || {};
|
|
2998
3044
|
const installedKeys = new Set(Object.keys(installRecords).map((k) => String(k || '').toLowerCase()));
|
|
2999
3045
|
const installedSpecs = new Set(Object.values(installRecords).flatMap((r) => {
|
|
@@ -3019,6 +3065,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3019
3065
|
const imageGenOn = !!cfg.skills?.entries?.['image-gen']?.enabled;
|
|
3020
3066
|
const webSearchOn = isEnabled(['duckduckgo']);
|
|
3021
3067
|
const stickerMentionOn = !!cfg.skills?.entries?.['sticker-mention']?.enabled;
|
|
3068
|
+
const learningMemoryOn = !!cfg.skills?.entries?.['learning-memory']?.enabled;
|
|
3022
3069
|
const aliases = {
|
|
3023
3070
|
browser: ['openclaw-browser-automation', 'browser-automation'],
|
|
3024
3071
|
zalo: ['openclaw-zalo-mod', 'zalo-mod'],
|
|
@@ -3031,6 +3078,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3031
3078
|
'skill:image-gen': imageGenOn,
|
|
3032
3079
|
'skill:web-search': webSearchOn,
|
|
3033
3080
|
'skill:sticker-mention': stickerMentionOn,
|
|
3081
|
+
'skill:learning-memory': learningMemoryOn,
|
|
3034
3082
|
'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
|
|
3035
3083
|
'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
|
|
3036
3084
|
'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
|
|
@@ -3044,6 +3092,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3044
3092
|
const installed = {
|
|
3045
3093
|
'skill:image-gen': isSkillFolderExists(projectDir, aid, 'infographic-generator', cfg),
|
|
3046
3094
|
'skill:sticker-mention': isSkillFolderExists(projectDir, aid, 'zalo-sticker-mention', cfg),
|
|
3095
|
+
'skill:learning-memory': isSkillFolderExists(projectDir, aid, 'learning-memory', cfg),
|
|
3047
3096
|
'plugin:openclaw-browser-automation': isActuallyInstalled(aliases.browser),
|
|
3048
3097
|
'plugin:openclaw-zalo-mod': isActuallyInstalled(aliases.zalo),
|
|
3049
3098
|
'plugin:openclaw-facebook-crawler': isActuallyInstalled(aliases.crawler),
|
|
@@ -3052,6 +3101,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3052
3101
|
const versions = {
|
|
3053
3102
|
'skill:image-gen': await getInstalledSkillVersion(projectDir, aid, 'infographic-generator', cfg),
|
|
3054
3103
|
'skill:sticker-mention': await getInstalledSkillVersion(projectDir, aid, 'zalo-sticker-mention', cfg),
|
|
3104
|
+
'skill:learning-memory': await getInstalledSkillVersion(projectDir, aid, 'learning-memory', cfg),
|
|
3055
3105
|
'plugin:openclaw-browser-automation': await getInstalledPluginVersion(projectDir, aliases.browser),
|
|
3056
3106
|
'plugin:openclaw-zalo-mod': await getInstalledPluginVersion(projectDir, aliases.zalo),
|
|
3057
3107
|
'plugin:openclaw-facebook-crawler': await getInstalledPluginVersion(projectDir, aliases.crawler),
|
|
@@ -3189,10 +3239,10 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3189
3239
|
const body = await readJson(req);
|
|
3190
3240
|
return json(res, await connectPickedProject(body.projectName, rootProjectDir));
|
|
3191
3241
|
}
|
|
3192
|
-
if (url.pathname === '/api/bot/status' && req.method === 'GET') {
|
|
3193
|
-
await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3194
|
-
return json(res, await buildBotStatus());
|
|
3195
|
-
}
|
|
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
|
+
}
|
|
3196
3246
|
if (url.pathname === '/api/bot/credentials' && req.method === 'PUT') {
|
|
3197
3247
|
const body = await readJson(req);
|
|
3198
3248
|
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
@@ -3319,36 +3369,36 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3319
3369
|
});
|
|
3320
3370
|
return json(res, { ok: true, message: 'Zalo login initiated. QR will appear in UI.' });
|
|
3321
3371
|
}
|
|
3322
|
-
if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
|
|
3323
|
-
const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
|
|
3324
|
-
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3325
|
-
const result = await deleteBotInProject(projectDir, agentId);
|
|
3326
|
-
sendLog(`? Bot deleted: ${agentId}`);
|
|
3327
|
-
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
|
|
3328
|
-
return json(res, result);
|
|
3329
|
-
}
|
|
3330
|
-
if (url.pathname === '/api/bot/files' && req.method === 'GET') {
|
|
3331
|
-
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3332
|
-
if (!projectDir) return json(res, { files: [] });
|
|
3333
|
-
const agentId = url.searchParams.get('agentId') || '';
|
|
3334
|
-
return json(res, { files: await listMarkdownFiles(projectDir, agentId).catch(() => []) });
|
|
3335
|
-
}
|
|
3336
|
-
if (url.pathname.startsWith('/api/bot/files/')) {
|
|
3337
|
-
const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
|
|
3338
|
-
if (req.method === 'GET') {
|
|
3339
|
-
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3340
|
-
const file = safeJoin(projectDir, name);
|
|
3341
|
-
return json(res, { name, content: await fsp.readFile(file, 'utf8') });
|
|
3342
|
-
}
|
|
3343
|
-
if (req.method === 'PUT') {
|
|
3344
|
-
if (!name.endsWith('.md')) throw httpError(400, 'Only markdown files (.md) can be modified');
|
|
3345
|
-
const body = await readJson(req);
|
|
3346
|
-
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3347
|
-
const file = safeJoin(projectDir, name);
|
|
3348
|
-
await fsp.writeFile(file, String(body.content || ''), 'utf8');
|
|
3349
|
-
return json(res, { ok: true });
|
|
3350
|
-
}
|
|
3351
|
-
}
|
|
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
|
+
}
|
|
3352
3402
|
if (url.pathname === '/api/catalog' && req.method === 'GET') return json(res, {
|
|
3353
3403
|
skills: [
|
|
3354
3404
|
{ name: 'Browser', slug: 'browser' },
|
|
@@ -3363,21 +3413,21 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3363
3413
|
{ name: 'openclaw-n8n-facebook-poster', package: 'openclaw-n8n-facebook-poster' },
|
|
3364
3414
|
]
|
|
3365
3415
|
});
|
|
3366
|
-
if (url.pathname === '/api/features' && req.method === 'GET') {
|
|
3367
|
-
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3368
|
-
if (!projectDir) return json(res, { flags: {}, installed: {}, versions: {} });
|
|
3369
|
-
return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
|
|
3370
|
-
}
|
|
3371
|
-
if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
|
|
3372
|
-
const body = await readJson(req);
|
|
3373
|
-
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3374
|
-
return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
|
|
3375
|
-
}
|
|
3376
|
-
if (url.pathname === '/api/features/install' && req.method === 'POST') {
|
|
3377
|
-
const body = await readJson(req);
|
|
3378
|
-
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3379
|
-
return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
|
|
3380
|
-
}
|
|
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
|
+
}
|
|
3381
3431
|
if (await serveStatic(req, res)) return;
|
|
3382
3432
|
json(res, { error: 'Not found' }, 404);
|
|
3383
3433
|
} catch (err) {
|