create-openclaw-bot 5.8.17 → 5.8.19
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.
|
@@ -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,8 +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
|
-
}
|
|
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
|
+
}
|
|
46
227
|
|
|
47
228
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
229
|
const WEB_DIR = resolve(__dirname, '../web');
|
|
@@ -88,6 +269,7 @@ const state = {
|
|
|
88
269
|
mode: null,
|
|
89
270
|
os: null,
|
|
90
271
|
startedAt: null,
|
|
272
|
+
projects: null,
|
|
91
273
|
};
|
|
92
274
|
|
|
93
275
|
function sendLog(line) {
|
|
@@ -113,13 +295,39 @@ function extractCompletePngBase64(stdout) {
|
|
|
113
295
|
return b64;
|
|
114
296
|
}
|
|
115
297
|
|
|
116
|
-
function detectOs() {
|
|
117
|
-
const platform = process.platform;
|
|
118
|
-
if (platform === 'win32') return 'win';
|
|
119
|
-
if (platform === 'darwin') return 'macos';
|
|
120
|
-
if (platform === 'linux') return os.release().toLowerCase().includes('microsoft') ? 'linux-desktop' : 'linux-desktop';
|
|
121
|
-
return 'linux-desktop';
|
|
122
|
-
}
|
|
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
|
+
}
|
|
123
331
|
|
|
124
332
|
function getRealHomedir() {
|
|
125
333
|
const home = os.homedir();
|
|
@@ -1074,7 +1282,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1074
1282
|
const emoji = String(body.emoji || '').trim();
|
|
1075
1283
|
const userName = String(body.userName || '').trim();
|
|
1076
1284
|
const userDesc = String(body.userDescription || body.userDesc || '').trim();
|
|
1077
|
-
const userInfo = [userName ? `- **
|
|
1285
|
+
const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
|
|
1078
1286
|
|
|
1079
1287
|
const openclawHome = join(projectDir, '.openclaw');
|
|
1080
1288
|
await fsp.mkdir(openclawHome, { recursive: true });
|
|
@@ -1607,8 +1815,9 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1607
1815
|
const botContainer = parseComposeServiceContainerName(compose, 'ai-bot') || `openclaw-${slugify(basename(projectDir))}`;
|
|
1608
1816
|
const routerContainer = parseComposeServiceContainerName(compose, '9router') || `9router-${slugify(basename(projectDir))}`;
|
|
1609
1817
|
const composeName = (compose.match(/^name:\s*(\S+)/m) || [])[1] || `oc-${slugify(basename(projectDir))}`;
|
|
1610
|
-
const gatewayPort = state.gatewayPort || 18789;
|
|
1611
|
-
const routerPort = state.routerPort || 20128;
|
|
1818
|
+
const gatewayPort = state.gatewayPort || 18789;
|
|
1819
|
+
const routerPort = state.routerPort || 20128;
|
|
1820
|
+
const osChoice = await resolveProjectHostOs(projectDir);
|
|
1612
1821
|
|
|
1613
1822
|
// Detect features from openclaw.json
|
|
1614
1823
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
@@ -1623,9 +1832,10 @@ async function syncDockerInfra(projectDir, force = false) {
|
|
|
1623
1832
|
is9Router: true,
|
|
1624
1833
|
openClawNpmSpec: OPENCLAW_NPM_SPEC,
|
|
1625
1834
|
openClawRuntimePackages: '',
|
|
1626
|
-
allSkills: [],
|
|
1627
|
-
dockerfilePlugins: [],
|
|
1628
|
-
|
|
1835
|
+
allSkills: [],
|
|
1836
|
+
dockerfilePlugins: [],
|
|
1837
|
+
osChoice,
|
|
1838
|
+
gatewayPort,
|
|
1629
1839
|
routerPort,
|
|
1630
1840
|
singleComposeName: composeName,
|
|
1631
1841
|
singleAppContainerName: botContainer,
|
|
@@ -1944,6 +2154,7 @@ async function saveState(rootProjectDir) {
|
|
|
1944
2154
|
gatewayPort: state.gatewayPort,
|
|
1945
2155
|
routerUrl: state.routerUrl,
|
|
1946
2156
|
routerPort: state.routerPort,
|
|
2157
|
+
projects: state.projects || [],
|
|
1947
2158
|
}, null, 2), 'utf8').catch(() => {});
|
|
1948
2159
|
}
|
|
1949
2160
|
|
|
@@ -1951,6 +2162,9 @@ async function loadSavedState(rootProjectDir) {
|
|
|
1951
2162
|
const file = join(rootProjectDir, STATE_FILE);
|
|
1952
2163
|
if (!existsSync(file)) return;
|
|
1953
2164
|
const saved = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
2165
|
+
if (Array.isArray(saved?.projects)) {
|
|
2166
|
+
state.projects = saved.projects;
|
|
2167
|
+
}
|
|
1954
2168
|
if (saved?.projectDir && existsSync(join(saved.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
1955
2169
|
Object.assign(state, saved, { installed: !!saved.installed });
|
|
1956
2170
|
await syncRuntimeState(state.projectDir);
|
|
@@ -1974,20 +2188,31 @@ function isRestrictedSystemDir(dirPath) {
|
|
|
1974
2188
|
if (lower.includes(':\\users\\') || lower.endsWith(':\\users')) {
|
|
1975
2189
|
const home = resolve(getRealHomedir()).toLowerCase();
|
|
1976
2190
|
if (lower !== home && !lower.startsWith(home + '\\') && !lower.startsWith(home + '/')) {
|
|
1977
|
-
|
|
2191
|
+
const cwd = resolve(process.cwd()).toLowerCase();
|
|
2192
|
+
const match = cwd.match(/^([a-z]:\\users\\[^\\]+)/) || cwd.match(/^(\/mnt\/[a-z]\/users\/[^\/]+)/);
|
|
2193
|
+
const cwdHome = match ? match[1] : '';
|
|
2194
|
+
if (!cwdHome || (lower !== cwdHome && !lower.startsWith(cwdHome + '\\') && !lower.startsWith(cwdHome + '/'))) {
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
1978
2197
|
}
|
|
1979
2198
|
}
|
|
1980
2199
|
|
|
1981
2200
|
if (process.platform !== 'win32') {
|
|
1982
2201
|
const unixBlacklist = new Set([
|
|
1983
|
-
'usr', 'var', 'proc', 'sys', 'dev', 'etc', 'sbin', 'bin', 'lib', 'lib64', 'run', 'tmp', 'boot', 'lost+found', 'srv', 'mnt', 'media', 'opt'
|
|
2202
|
+
'usr', 'var', 'proc', 'sys', 'dev', 'etc', 'sbin', 'bin', 'lib', 'lib64', 'run', 'tmp', 'boot', 'lost+found', 'srv', 'mnt', 'media', 'opt',
|
|
2203
|
+
'applications', 'library', 'system', 'volumes', 'private', 'cores', 'network', 'users'
|
|
1984
2204
|
]);
|
|
1985
2205
|
if (unixBlacklist.has(basename(lower))) return true;
|
|
1986
2206
|
|
|
1987
|
-
if (lower.startsWith('/home') || lower.startsWith('/root')) {
|
|
2207
|
+
if (lower.startsWith('/home') || lower.startsWith('/root') || lower.startsWith('/mnt/') || lower.startsWith('/users/') || lower === '/users') {
|
|
1988
2208
|
const realHome = resolve(getRealHomedir()).toLowerCase();
|
|
1989
2209
|
if (lower !== realHome && !lower.startsWith(realHome + '/')) {
|
|
1990
|
-
|
|
2210
|
+
const cwd = resolve(process.cwd()).toLowerCase();
|
|
2211
|
+
const match = cwd.match(/^(\/home\/[^\/]+)/) || cwd.match(/^(\/root)/) || cwd.match(/^(\/mnt\/[a-z]\/users\/[^\/]+)/) || cwd.match(/^(\/users\/[^\/]+)/);
|
|
2212
|
+
const cwdHome = match ? match[1] : '';
|
|
2213
|
+
if (!cwdHome || (lower !== cwdHome && !lower.startsWith(cwdHome + '/'))) {
|
|
2214
|
+
return true;
|
|
2215
|
+
}
|
|
1991
2216
|
}
|
|
1992
2217
|
}
|
|
1993
2218
|
}
|
|
@@ -2045,108 +2270,114 @@ async function findLatestProject(rootProjectDir) {
|
|
|
2045
2270
|
return candidates[0]?.dir || null;
|
|
2046
2271
|
}
|
|
2047
2272
|
|
|
2048
|
-
async function
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
].filter(Boolean);
|
|
2058
|
-
|
|
2059
|
-
const drives = await getAvailableDrives();
|
|
2060
|
-
for (const drive of drives) {
|
|
2061
|
-
const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
|
|
2062
|
-
for (const e of entries) {
|
|
2063
|
-
if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
|
|
2064
|
-
const fullPath = join(drive, e.name);
|
|
2065
|
-
if (!isRestrictedSystemDir(fullPath)) {
|
|
2066
|
-
roots.push(fullPath);
|
|
2067
|
-
}
|
|
2273
|
+
async function ensureProjectsLoaded(rootProjectDir) {
|
|
2274
|
+
if (state.projects !== null) return;
|
|
2275
|
+
state.projects = [];
|
|
2276
|
+
const file = join(rootProjectDir, STATE_FILE);
|
|
2277
|
+
if (existsSync(file)) {
|
|
2278
|
+
try {
|
|
2279
|
+
const saved = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
2280
|
+
if (Array.isArray(saved?.projects)) {
|
|
2281
|
+
state.projects = saved.projects;
|
|
2068
2282
|
}
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
const seen = new Set();
|
|
2072
|
-
const hits = [];
|
|
2073
|
-
async function walk(dir, depth = 0) {
|
|
2074
|
-
if (!dir || depth > 2 || !existsSync(dir)) return;
|
|
2075
|
-
const full = resolve(dir);
|
|
2076
|
-
if (isRestrictedSystemDir(full)) return;
|
|
2077
|
-
if (seen.has(full)) return;
|
|
2078
|
-
seen.add(full);
|
|
2079
|
-
const cfgPath = join(full, '.openclaw', 'openclaw.json');
|
|
2080
|
-
if (existsSync(cfgPath)) {
|
|
2081
|
-
const st = await fsp.stat(cfgPath).catch(() => null);
|
|
2082
|
-
const runtime = await detectRuntime(full).catch(() => ({ mode: 'unknown', gatewayPort: 0, routerPort: 0, syncSource: 'config' }));
|
|
2083
|
-
const bots = await listConfiguredBots(full).catch(() => []);
|
|
2084
|
-
const uniqueBotCount = new Set(bots.map((b) => b.id)).size;
|
|
2085
|
-
const hasDocker = existsSync(join(full, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2086
|
-
const isLikelyProject = uniqueBotCount > 0 || hasDocker || existsSync(join(full, '.env')) || existsSync(join(full, 'package.json'));
|
|
2087
|
-
if (!isLikelyProject) return;
|
|
2088
|
-
hits.push({
|
|
2089
|
-
projectDir: full,
|
|
2090
|
-
os: process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux',
|
|
2091
|
-
mode: runtime.mode || 'unknown',
|
|
2092
|
-
gatewayPort: runtime.gatewayPort || 0,
|
|
2093
|
-
routerPort: runtime.routerPort || 0,
|
|
2094
|
-
syncSource: runtime.syncSource || 'config',
|
|
2095
|
-
botCount: uniqueBotCount,
|
|
2096
|
-
hasDocker,
|
|
2097
|
-
updatedAt: st?.mtimeMs || 0,
|
|
2098
|
-
});
|
|
2099
|
-
return;
|
|
2100
|
-
}
|
|
2101
|
-
const entries = await fsp.readdir(full, { withFileTypes: true }).catch(() => []);
|
|
2102
|
-
for (const e of entries) {
|
|
2103
|
-
if (!e.isDirectory()) continue;
|
|
2104
|
-
if (e.name === 'node_modules' || e.name.startsWith('.') || SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) continue;
|
|
2105
|
-
await walk(join(full, e.name), depth + 1);
|
|
2106
|
-
}
|
|
2283
|
+
} catch {}
|
|
2107
2284
|
}
|
|
2108
|
-
for (const root of roots) await walk(root);
|
|
2109
|
-
hits.sort((a, b) =>
|
|
2110
|
-
(b.botCount - a.botCount) ||
|
|
2111
|
-
(Number(b.hasDocker) - Number(a.hasDocker)) ||
|
|
2112
|
-
(b.updatedAt - a.updatedAt)
|
|
2113
|
-
);
|
|
2114
|
-
return hits.slice(0, 20);
|
|
2115
2285
|
}
|
|
2116
2286
|
|
|
2117
|
-
async function
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
await syncRuntimeState(state.projectDir);
|
|
2121
|
-
return state.projectDir;
|
|
2122
|
-
}
|
|
2123
|
-
const envProjectDir = process.env.OPENCLAW_PROJECT_DIR || (process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '');
|
|
2124
|
-
if (envProjectDir && existsSync(join(resolve(String(envProjectDir)), '.openclaw', 'openclaw.json'))) {
|
|
2125
|
-
state.projectDir = resolve(String(envProjectDir));
|
|
2126
|
-
await syncRuntimeState(state.projectDir);
|
|
2127
|
-
return state.projectDir;
|
|
2128
|
-
}
|
|
2129
|
-
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2130
|
-
await syncRuntimeState(state.projectDir);
|
|
2131
|
-
return state.projectDir;
|
|
2132
|
-
}
|
|
2133
|
-
await loadSavedState(rootProjectDir);
|
|
2287
|
+
async function discoverProjects(rootProjectDir) {
|
|
2288
|
+
await ensureProjectsLoaded(rootProjectDir);
|
|
2289
|
+
|
|
2134
2290
|
if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2135
|
-
|
|
2136
|
-
|
|
2291
|
+
const resolved = resolve(state.projectDir);
|
|
2292
|
+
if (!state.projects.some(p => resolve(p.projectDir) === resolved)) {
|
|
2293
|
+
const meta = await buildProjectMeta(resolved).catch(() => null);
|
|
2294
|
+
if (meta) state.projects.push(meta);
|
|
2295
|
+
}
|
|
2137
2296
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2297
|
+
|
|
2298
|
+
const updatedProjects = [];
|
|
2299
|
+
for (const p of state.projects) {
|
|
2300
|
+
if (existsSync(join(p.projectDir, '.openclaw', 'openclaw.json'))) {
|
|
2301
|
+
const meta = await buildProjectMeta(p.projectDir).catch(() => null);
|
|
2302
|
+
if (meta) {
|
|
2303
|
+
updatedProjects.push(meta);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2142
2306
|
}
|
|
2307
|
+
state.projects = updatedProjects;
|
|
2308
|
+
|
|
2309
|
+
state.projects.sort((a, b) => {
|
|
2310
|
+
const aActive = state.projectDir && resolve(state.projectDir) === resolve(a.projectDir);
|
|
2311
|
+
const bActive = state.projectDir && resolve(state.projectDir) === resolve(b.projectDir);
|
|
2312
|
+
if (aActive && !bActive) return -1;
|
|
2313
|
+
if (!aActive && bActive) return 1;
|
|
2314
|
+
return (b.botCount - a.botCount) || (b.updatedAt - a.updatedAt);
|
|
2315
|
+
});
|
|
2316
|
+
|
|
2317
|
+
await saveState(rootProjectDir).catch(() => {});
|
|
2318
|
+
return state.projects.slice(0, 20);
|
|
2319
|
+
}
|
|
2320
|
+
|
|
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
|
+
}
|
|
2143
2347
|
return state.projectDir;
|
|
2144
2348
|
}
|
|
2145
2349
|
|
|
2350
|
+
async function buildProjectMeta(projectDir) {
|
|
2351
|
+
const full = resolve(projectDir);
|
|
2352
|
+
const cfgPath = join(full, '.openclaw', 'openclaw.json');
|
|
2353
|
+
const st = await fsp.stat(cfgPath).catch(() => null);
|
|
2354
|
+
const runtime = await detectRuntime(full).catch(() => ({ mode: 'unknown', gatewayPort: 0, routerPort: 0, syncSource: 'config' }));
|
|
2355
|
+
const bots = await listConfiguredBots(full).catch(() => []);
|
|
2356
|
+
const uniqueBotCount = new Set(bots.map((b) => b.id)).size;
|
|
2357
|
+
const hasDocker = existsSync(join(full, 'docker', 'openclaw', 'docker-compose.yml'));
|
|
2358
|
+
return {
|
|
2359
|
+
projectDir: full,
|
|
2360
|
+
os: process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux',
|
|
2361
|
+
mode: runtime.mode || 'unknown',
|
|
2362
|
+
gatewayPort: runtime.gatewayPort || 0,
|
|
2363
|
+
routerPort: runtime.routerPort || 0,
|
|
2364
|
+
syncSource: runtime.syncSource || 'config',
|
|
2365
|
+
botCount: uniqueBotCount,
|
|
2366
|
+
hasDocker,
|
|
2367
|
+
updatedAt: st?.mtimeMs || 0,
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2146
2371
|
async function connectExistingProject(projectDir, rootProjectDir) {
|
|
2147
2372
|
const resolved = resolve(String(projectDir || ''));
|
|
2148
2373
|
if (!existsSync(join(resolved, '.openclaw', 'openclaw.json'))) throw httpError(404, 'openclaw.json not found in selected project');
|
|
2149
2374
|
await syncRuntimeState(resolved);
|
|
2375
|
+
await ensureProjectsLoaded(rootProjectDir);
|
|
2376
|
+
const meta = await buildProjectMeta(resolved).catch(() => null);
|
|
2377
|
+
if (meta) {
|
|
2378
|
+
state.projects = state.projects.filter(p => resolve(p.projectDir) !== resolved);
|
|
2379
|
+
state.projects.unshift(meta);
|
|
2380
|
+
}
|
|
2150
2381
|
await saveState(rootProjectDir);
|
|
2151
2382
|
const bots = await listConfiguredBots(resolved).catch(() => []);
|
|
2152
2383
|
return {
|
|
@@ -2200,6 +2431,8 @@ async function deleteProjectFolder(projectDir, rootProjectDir) {
|
|
|
2200
2431
|
state.projectDir = null;
|
|
2201
2432
|
state.installed = false;
|
|
2202
2433
|
}
|
|
2434
|
+
await ensureProjectsLoaded(rootProjectDir);
|
|
2435
|
+
state.projects = state.projects.filter(p => resolve(p.projectDir) !== resolved);
|
|
2203
2436
|
await saveState(rootProjectDir);
|
|
2204
2437
|
return { ok: true, projectDir: resolved };
|
|
2205
2438
|
}
|
|
@@ -2263,15 +2496,17 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
2263
2496
|
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
2264
2497
|
if (enabled) {
|
|
2265
2498
|
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2266
|
-
} else {
|
|
2267
|
-
cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
|
|
2268
|
-
for (const a of cfg.agents.list) {
|
|
2269
|
-
const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
|
|
2270
|
-
if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
|
|
2271
|
-
const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
|
|
2272
|
-
if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
|
|
2273
|
-
|
|
2274
|
-
|
|
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
|
+
}
|
|
2275
2510
|
|
|
2276
2511
|
// Write cfgPath early so syncDockerInfra reads the updated openclaw.json
|
|
2277
2512
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
@@ -2562,12 +2797,17 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2562
2797
|
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2563
2798
|
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2564
2799
|
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2565
|
-
const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
|
|
2566
|
-
cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
|
|
2567
|
-
cfg.plugins.entries[existingKey].enabled = true;
|
|
2568
|
-
if (existingKey === '
|
|
2569
|
-
|
|
2570
|
-
|
|
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
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2809
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2810
|
+
}
|
|
2571
2811
|
// Only add the canonical config key to allow list (not all aliases)
|
|
2572
2812
|
if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
|
|
2573
2813
|
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
@@ -2595,14 +2835,15 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2595
2835
|
}
|
|
2596
2836
|
}
|
|
2597
2837
|
|
|
2598
|
-
// Browser-automation plugin needs Docker rebuild for Playwright/Chromium deps
|
|
2599
|
-
const isBrowserPlugin = id === 'openclaw-browser-automation' || id === 'browser-automation';
|
|
2600
|
-
if (isBrowserPlugin && composeDir) {
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
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 });
|
|
2606
2847
|
});
|
|
2607
2848
|
} else if (isZaloMod && composeDir) {
|
|
2608
2849
|
// Use docker compose up to apply new port mappings from docker-compose.yml
|
|
@@ -2638,26 +2879,37 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
2638
2879
|
|
|
2639
2880
|
// Automatically enable it in config after install
|
|
2640
2881
|
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
2641
|
-
if (existsSync(cfgPath)) {
|
|
2642
|
-
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
2643
|
-
cfg.plugins = cfg.plugins || { entries: {} };
|
|
2644
|
-
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
2645
|
-
const pluginAliasMap = {
|
|
2646
|
-
'openclaw-
|
|
2647
|
-
'openclaw-
|
|
2648
|
-
'openclaw-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
const
|
|
2652
|
-
|
|
2653
|
-
cfg.plugins.entries[existingKey]
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
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
|
+
if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
|
|
2902
|
+
ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
|
|
2903
|
+
}
|
|
2904
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
if (id === 'openclaw-browser-automation' || id === 'browser-automation') {
|
|
2908
|
+
const aliases = ['browser-automation', 'openclaw-browser-automation'];
|
|
2909
|
+
await patchBrowserAutomationHostPreference(projectDir, aliases, sendLog);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2661
2913
|
return { ok: true };
|
|
2662
2914
|
}
|
|
2663
2915
|
|
|
@@ -2931,10 +3183,10 @@ async function handler(req, res, rootProjectDir) {
|
|
|
2931
3183
|
const body = await readJson(req);
|
|
2932
3184
|
return json(res, await connectPickedProject(body.projectName, rootProjectDir));
|
|
2933
3185
|
}
|
|
2934
|
-
if (url.pathname === '/api/bot/status' && req.method === 'GET') {
|
|
2935
|
-
await resolveProjectDir(rootProjectDir);
|
|
2936
|
-
return json(res, await buildBotStatus());
|
|
2937
|
-
}
|
|
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
|
+
}
|
|
2938
3190
|
if (url.pathname === '/api/bot/credentials' && req.method === 'PUT') {
|
|
2939
3191
|
const body = await readJson(req);
|
|
2940
3192
|
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
@@ -3061,29 +3313,34 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3061
3313
|
});
|
|
3062
3314
|
return json(res, { ok: true, message: 'Zalo login initiated. QR will appear in UI.' });
|
|
3063
3315
|
}
|
|
3064
|
-
if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
|
|
3065
|
-
const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
|
|
3066
|
-
const projectDir = await resolveProjectDir(rootProjectDir);
|
|
3067
|
-
const result = await deleteBotInProject(projectDir, agentId);
|
|
3068
|
-
sendLog(`? Bot deleted: ${agentId}`);
|
|
3069
|
-
await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
|
|
3070
|
-
return json(res, result);
|
|
3071
|
-
}
|
|
3072
|
-
if (url.pathname === '/api/bot/files' && req.method === 'GET') {
|
|
3073
|
-
await resolveProjectDir(rootProjectDir);
|
|
3074
|
-
return json(res, { files: state.projectDir ? await listMarkdownFiles(state.projectDir, url.searchParams.get('agentId') || '') : [] });
|
|
3075
|
-
}
|
|
3076
|
-
if (url.pathname.startsWith('/api/bot/files/')
|
|
3077
|
-
const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
+
await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3326
|
+
return json(res, { files: state.projectDir ? await listMarkdownFiles(state.projectDir, url.searchParams.get('agentId') || '') : [] });
|
|
3327
|
+
}
|
|
3328
|
+
if (url.pathname.startsWith('/api/bot/files/')) {
|
|
3329
|
+
const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
|
|
3330
|
+
if (req.method === 'GET') {
|
|
3331
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3332
|
+
const file = safeJoin(projectDir, name);
|
|
3333
|
+
return json(res, { name, content: await fsp.readFile(file, 'utf8') });
|
|
3334
|
+
}
|
|
3335
|
+
if (req.method === 'PUT') {
|
|
3336
|
+
if (!name.endsWith('.md')) throw httpError(400, 'Only markdown files (.md) can be modified');
|
|
3337
|
+
const body = await readJson(req);
|
|
3338
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3339
|
+
const file = safeJoin(projectDir, name);
|
|
3340
|
+
await fsp.writeFile(file, String(body.content || ''), 'utf8');
|
|
3341
|
+
return json(res, { ok: true });
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3087
3344
|
if (url.pathname === '/api/catalog' && req.method === 'GET') return json(res, {
|
|
3088
3345
|
skills: [
|
|
3089
3346
|
{ name: 'Browser', slug: 'browser' },
|
|
@@ -3098,20 +3355,20 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3098
3355
|
{ name: 'openclaw-n8n-facebook-poster', package: 'openclaw-n8n-facebook-poster' },
|
|
3099
3356
|
]
|
|
3100
3357
|
});
|
|
3101
|
-
if (url.pathname === '/api/features' && req.method === 'GET') {
|
|
3102
|
-
const projectDir = await resolveProjectDir(rootProjectDir);
|
|
3103
|
-
return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
|
|
3104
|
-
}
|
|
3105
|
-
if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
|
|
3106
|
-
const body = await readJson(req);
|
|
3107
|
-
const projectDir = await resolveProjectDir(rootProjectDir);
|
|
3108
|
-
return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
|
|
3109
|
-
}
|
|
3110
|
-
if (url.pathname === '/api/features/install' && req.method === 'POST') {
|
|
3111
|
-
const body = await readJson(req);
|
|
3112
|
-
const projectDir = await resolveProjectDir(rootProjectDir);
|
|
3113
|
-
return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
|
|
3114
|
-
}
|
|
3358
|
+
if (url.pathname === '/api/features' && req.method === 'GET') {
|
|
3359
|
+
const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
|
|
3360
|
+
return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
|
|
3361
|
+
}
|
|
3362
|
+
if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
|
|
3363
|
+
const body = await readJson(req);
|
|
3364
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3365
|
+
return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
|
|
3366
|
+
}
|
|
3367
|
+
if (url.pathname === '/api/features/install' && req.method === 'POST') {
|
|
3368
|
+
const body = await readJson(req);
|
|
3369
|
+
const projectDir = await resolveProjectDir(rootProjectDir, body);
|
|
3370
|
+
return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
|
|
3371
|
+
}
|
|
3115
3372
|
if (await serveStatic(req, res)) return;
|
|
3116
3373
|
json(res, { error: 'Not found' }, 404);
|
|
3117
3374
|
} catch (err) {
|