create-openclaw-bot 5.8.18 → 5.8.20

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');
@@ -114,13 +295,39 @@ function extractCompletePngBase64(stdout) {
114
295
  return b64;
115
296
  }
116
297
 
117
- function detectOs() {
118
- const platform = process.platform;
119
- if (platform === 'win32') return 'win';
120
- if (platform === 'darwin') return 'macos';
121
- if (platform === 'linux') return os.release().toLowerCase().includes('microsoft') ? 'linux-desktop' : 'linux-desktop';
122
- return 'linux-desktop';
123
- }
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
+ }
124
331
 
125
332
  function getRealHomedir() {
126
333
  const home = os.homedir();
@@ -1608,8 +1815,9 @@ async function syncDockerInfra(projectDir, force = false) {
1608
1815
  const botContainer = parseComposeServiceContainerName(compose, 'ai-bot') || `openclaw-${slugify(basename(projectDir))}`;
1609
1816
  const routerContainer = parseComposeServiceContainerName(compose, '9router') || `9router-${slugify(basename(projectDir))}`;
1610
1817
  const composeName = (compose.match(/^name:\s*(\S+)/m) || [])[1] || `oc-${slugify(basename(projectDir))}`;
1611
- const gatewayPort = state.gatewayPort || 18789;
1612
- const routerPort = state.routerPort || 20128;
1818
+ const gatewayPort = state.gatewayPort || 18789;
1819
+ const routerPort = state.routerPort || 20128;
1820
+ const osChoice = await resolveProjectHostOs(projectDir);
1613
1821
 
1614
1822
  // Detect features from openclaw.json
1615
1823
  const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
@@ -1624,9 +1832,10 @@ async function syncDockerInfra(projectDir, force = false) {
1624
1832
  is9Router: true,
1625
1833
  openClawNpmSpec: OPENCLAW_NPM_SPEC,
1626
1834
  openClawRuntimePackages: '',
1627
- allSkills: [],
1628
- dockerfilePlugins: [],
1629
- gatewayPort,
1835
+ allSkills: [],
1836
+ dockerfilePlugins: [],
1837
+ osChoice,
1838
+ gatewayPort,
1630
1839
  routerPort,
1631
1840
  singleComposeName: composeName,
1632
1841
  singleAppContainerName: botContainer,
@@ -2109,32 +2318,32 @@ async function discoverProjects(rootProjectDir) {
2109
2318
  return state.projects.slice(0, 20);
2110
2319
  }
2111
2320
 
2112
- async function resolveProjectDir(rootProjectDir, body = {}) {
2113
- if (body.projectDir && existsSync(join(resolve(String(body.projectDir)), '.openclaw', 'openclaw.json'))) {
2114
- state.projectDir = resolve(String(body.projectDir));
2115
- await syncRuntimeState(state.projectDir);
2116
- return state.projectDir;
2117
- }
2118
- const envProjectDir = process.env.OPENCLAW_PROJECT_DIR || (process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '');
2119
- if (envProjectDir && existsSync(join(resolve(String(envProjectDir)), '.openclaw', 'openclaw.json'))) {
2120
- state.projectDir = resolve(String(envProjectDir));
2121
- await syncRuntimeState(state.projectDir);
2122
- return state.projectDir;
2123
- }
2124
- if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
2125
- await syncRuntimeState(state.projectDir);
2126
- return state.projectDir;
2127
- }
2128
- await loadSavedState(rootProjectDir);
2129
- if (state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json'))) {
2130
- await syncRuntimeState(state.projectDir);
2131
- return state.projectDir;
2132
- }
2133
- const found = await findLatestProject(rootProjectDir);
2134
- if (found) {
2135
- await syncRuntimeState(found);
2136
- await saveState(rootProjectDir);
2137
- }
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
+ }
2138
2347
  return state.projectDir;
2139
2348
  }
2140
2349
 
@@ -2287,15 +2496,17 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2287
2496
  cfg.plugins.allow = cfg.plugins.allow || [];
2288
2497
  if (enabled) {
2289
2498
  if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
2290
- } else {
2291
- cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
2292
- for (const a of cfg.agents.list) {
2293
- const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
2294
- if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
2295
- const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
2296
- if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
2297
- }
2298
- }
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
+ }
2299
2510
 
2300
2511
  // Write cfgPath early so syncDockerInfra reads the updated openclaw.json
2301
2512
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
@@ -2586,12 +2797,17 @@ async function installFeature(projectDir, agentId, kind, id) {
2586
2797
  const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
2587
2798
  cfg.plugins = cfg.plugins || { entries: {} };
2588
2799
  cfg.plugins.entries = cfg.plugins.entries || {};
2589
- const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
2590
- cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
2591
- cfg.plugins.entries[existingKey].enabled = true;
2592
- if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
2593
- ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
2594
- }
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
+ }
2595
2811
  // Only add the canonical config key to allow list (not all aliases)
2596
2812
  if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
2597
2813
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
@@ -2619,14 +2835,15 @@ async function installFeature(projectDir, agentId, kind, id) {
2619
2835
  }
2620
2836
  }
2621
2837
 
2622
- // Browser-automation plugin needs Docker rebuild for Playwright/Chromium deps
2623
- const isBrowserPlugin = id === 'openclaw-browser-automation' || id === 'browser-automation';
2624
- if (isBrowserPlugin && composeDir) {
2625
- sendLog(`[plugin] Browser plugin requires Docker rebuild for Playwright/Chromium...`);
2626
- const svcName = getBotServiceName(projectDir);
2627
- await run('docker', ['compose', '-f', join(composeDir, 'docker-compose.yml'), 'up', '-d', '--build', '--force-recreate', svcName], { shell: false }).catch((err) => {
2628
- sendLog(`[plugin] Docker rebuild failed: ${err.message}. Falling back to restart...`);
2629
- return run('docker', ['restart', botContainer], { shell: false });
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 });
2630
2847
  });
2631
2848
  } else if (isZaloMod && composeDir) {
2632
2849
  // Use docker compose up to apply new port mappings from docker-compose.yml
@@ -2662,26 +2879,37 @@ async function installFeature(projectDir, agentId, kind, id) {
2662
2879
 
2663
2880
  // Automatically enable it in config after install
2664
2881
  const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
2665
- if (existsSync(cfgPath)) {
2666
- const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
2667
- cfg.plugins = cfg.plugins || { entries: {} };
2668
- cfg.plugins.entries = cfg.plugins.entries || {};
2669
- const pluginAliasMap = {
2670
- 'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
2671
- 'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
2672
- 'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
2673
- };
2674
- const aliases = pluginAliasMap[id] || [id];
2675
- const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
2676
- cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
2677
- cfg.plugins.entries[existingKey].enabled = true;
2678
- if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
2679
- ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
2680
- }
2681
- await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2682
- }
2683
- }
2684
- }
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
+ }
2685
2913
  return { ok: true };
2686
2914
  }
2687
2915
 
@@ -2955,10 +3183,10 @@ async function handler(req, res, rootProjectDir) {
2955
3183
  const body = await readJson(req);
2956
3184
  return json(res, await connectPickedProject(body.projectName, rootProjectDir));
2957
3185
  }
2958
- if (url.pathname === '/api/bot/status' && req.method === 'GET') {
2959
- await resolveProjectDir(rootProjectDir);
2960
- return json(res, await buildBotStatus());
2961
- }
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
+ }
2962
3190
  if (url.pathname === '/api/bot/credentials' && req.method === 'PUT') {
2963
3191
  const body = await readJson(req);
2964
3192
  const projectDir = await resolveProjectDir(rootProjectDir, body);
@@ -3085,29 +3313,36 @@ async function handler(req, res, rootProjectDir) {
3085
3313
  });
3086
3314
  return json(res, { ok: true, message: 'Zalo login initiated. QR will appear in UI.' });
3087
3315
  }
3088
- if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
3089
- const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
3090
- const projectDir = await resolveProjectDir(rootProjectDir);
3091
- const result = await deleteBotInProject(projectDir, agentId);
3092
- sendLog(`? Bot deleted: ${agentId}`);
3093
- await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
3094
- return json(res, result);
3095
- }
3096
- if (url.pathname === '/api/bot/files' && req.method === 'GET') {
3097
- await resolveProjectDir(rootProjectDir);
3098
- return json(res, { files: state.projectDir ? await listMarkdownFiles(state.projectDir, url.searchParams.get('agentId') || '') : [] });
3099
- }
3100
- if (url.pathname.startsWith('/api/bot/files/') && state.projectDir) {
3101
- const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
3102
- const file = safeJoin(state.projectDir, name);
3103
- if (req.method === 'GET') return json(res, { name, content: await fsp.readFile(file, 'utf8') });
3104
- if (req.method === 'PUT') {
3105
- if (!name.endsWith('.md')) throw httpError(400, 'Only markdown files (.md) can be modified');
3106
- const body = await readJson(req);
3107
- await fsp.writeFile(file, String(body.content || ''), 'utf8');
3108
- return json(res, { ok: true });
3109
- }
3110
- }
3316
+ if (url.pathname.startsWith('/api/bot/') && req.method === 'DELETE' && !url.pathname.startsWith('/api/bot/files/')) {
3317
+ const agentId = decodeURIComponent(url.pathname.replace('/api/bot/', ''));
3318
+ const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
3319
+ const result = await deleteBotInProject(projectDir, agentId);
3320
+ sendLog(`? Bot deleted: ${agentId}`);
3321
+ await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] recreate skipped/failed: ${err.message}`));
3322
+ return json(res, result);
3323
+ }
3324
+ if (url.pathname === '/api/bot/files' && req.method === 'GET') {
3325
+ const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
3326
+ if (!projectDir) return json(res, { files: [] });
3327
+ const agentId = url.searchParams.get('agentId') || '';
3328
+ return json(res, { files: await listMarkdownFiles(projectDir, agentId).catch(() => []) });
3329
+ }
3330
+ if (url.pathname.startsWith('/api/bot/files/')) {
3331
+ const name = decodeURIComponent(url.pathname.replace('/api/bot/files/', ''));
3332
+ if (req.method === 'GET') {
3333
+ const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
3334
+ const file = safeJoin(projectDir, name);
3335
+ return json(res, { name, content: await fsp.readFile(file, 'utf8') });
3336
+ }
3337
+ if (req.method === 'PUT') {
3338
+ if (!name.endsWith('.md')) throw httpError(400, 'Only markdown files (.md) can be modified');
3339
+ const body = await readJson(req);
3340
+ const projectDir = await resolveProjectDir(rootProjectDir, body);
3341
+ const file = safeJoin(projectDir, name);
3342
+ await fsp.writeFile(file, String(body.content || ''), 'utf8');
3343
+ return json(res, { ok: true });
3344
+ }
3345
+ }
3111
3346
  if (url.pathname === '/api/catalog' && req.method === 'GET') return json(res, {
3112
3347
  skills: [
3113
3348
  { name: 'Browser', slug: 'browser' },
@@ -3122,20 +3357,21 @@ async function handler(req, res, rootProjectDir) {
3122
3357
  { name: 'openclaw-n8n-facebook-poster', package: 'openclaw-n8n-facebook-poster' },
3123
3358
  ]
3124
3359
  });
3125
- if (url.pathname === '/api/features' && req.method === 'GET') {
3126
- const projectDir = await resolveProjectDir(rootProjectDir);
3127
- return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
3128
- }
3129
- if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
3130
- const body = await readJson(req);
3131
- const projectDir = await resolveProjectDir(rootProjectDir);
3132
- return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
3133
- }
3134
- if (url.pathname === '/api/features/install' && req.method === 'POST') {
3135
- const body = await readJson(req);
3136
- const projectDir = await resolveProjectDir(rootProjectDir);
3137
- return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
3138
- }
3360
+ if (url.pathname === '/api/features' && req.method === 'GET') {
3361
+ const projectDir = await resolveProjectDir(rootProjectDir, Object.fromEntries(url.searchParams));
3362
+ if (!projectDir) return json(res, { flags: {}, installed: {}, versions: {} });
3363
+ return json(res, await getFeatureFlags(projectDir, url.searchParams.get('agentId') || ''));
3364
+ }
3365
+ if (url.pathname === '/api/features/toggle' && req.method === 'POST') {
3366
+ const body = await readJson(req);
3367
+ const projectDir = await resolveProjectDir(rootProjectDir, body);
3368
+ return json(res, await applyFeatureToggle(projectDir, body.agentId || '', body.kind, body.id, !!body.enabled));
3369
+ }
3370
+ if (url.pathname === '/api/features/install' && req.method === 'POST') {
3371
+ const body = await readJson(req);
3372
+ const projectDir = await resolveProjectDir(rootProjectDir, body);
3373
+ return json(res, await installFeature(projectDir, body.agentId || '', body.kind, body.id));
3374
+ }
3139
3375
  if (await serveStatic(req, res)) return;
3140
3376
  json(res, { error: 'Not found' }, 404);
3141
3377
  } catch (err) {
@@ -91,7 +91,11 @@
91
91
  }));
92
92
 
93
93
  const cfg = {
94
- meta: { lastTouchedVersion: (_common.OPENCLAW_NPM_SPEC || 'latest').replace('openclaw@', '') },
94
+ meta: {
95
+ lastTouchedVersion: (_common.OPENCLAW_NPM_SPEC || 'latest').replace('openclaw@', ''),
96
+ osChoice,
97
+ deployMode,
98
+ },
95
99
  agents: {
96
100
  defaults: {
97
101
  model: { primary: model, fallbacks: [] },
@@ -221,11 +221,12 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
221
221
  singleOllamaContainerName = 'ollama',
222
222
  multiOllamaContainerName = 'ollama-multibot',
223
223
  plainSingleExtraHosts = false,
224
- multiOllamaNumParallel = 1,
225
- singleOllamaNumParallel = 1,
226
- gatewayPort = 18789,
227
- routerPort = 20128,
228
- } = options;
224
+ multiOllamaNumParallel = 1,
225
+ singleOllamaNumParallel = 1,
226
+ gatewayPort = 18789,
227
+ routerPort = 20128,
228
+ osChoice = '',
229
+ } = options;
229
230
  const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
230
231
  ? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
231
232
  : '';
@@ -355,7 +356,7 @@ CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
355
356
  const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
356
357
  const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
357
358
 
358
- const appEnvironmentBlock = ` environment:\n - HOME=/home/node/project/.openclaw\n - OPENCLAW_HOME=/home/node/project/.openclaw\n - OPENCLAW_STATE_DIR=/home/node/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n - OPENCLAW_GATEWAY_PORT=${gatewayPort}\n - OPENCLAW_PORT=${gatewayPort}\n tmpfs:\n - /home/node/project/.openclaw/plugin-runtime-deps\n`;
359
+ const appEnvironmentBlock = ` environment:\n - HOME=/home/node/project/.openclaw\n - OPENCLAW_HOME=/home/node/project/.openclaw\n - OPENCLAW_STATE_DIR=/home/node/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n - OPENCLAW_SETUP_OS=${osChoice || ''}\n - OPENCLAW_BROWSER_HOST_OS=${osChoice || ''}\n - OPENCLAW_GATEWAY_PORT=${gatewayPort}\n - OPENCLAW_PORT=${gatewayPort}\n tmpfs:\n - /home/node/project/.openclaw/plugin-runtime-deps\n`;
359
360
 
360
361
  let compose;
361
362
  if (isMultiBot) {
@@ -330,79 +330,61 @@ description: Startup and initialization instructions
330
330
  - [Operating Manual](./AGENTS.md)`;
331
331
  }
332
332
 
333
- function buildCronjobSkillMd(isVi = true) {
334
- if (isVi) {
335
- return `---
336
- name: cronjob
337
- description: Lên lịch tác vụ định kỳ sử dụng công cụ cron.
338
- ---
339
-
340
- # ⏰ Cron / Lên lịch nhắc nhở (tool: \`cron\`)
341
- - **Tên tool chính xác:** Tên công cụ là \`cron\` (tuyệt đối không nhầm là \`native\` hay command line bên ngoài).
342
- - **⛔ TUYỆT ĐỐI KHÔNG sửa trực tiếp file JSON** như \`jobs.json\`, \`jobs-state.json\` trong thư mục \`.openclaw/cron/\`. Dữ liệu cron được lưu trong SQLite database, file JSON chỉ là legacy format đã ngưng hỗ trợ. Mọi thao tác PHẢI thông qua tool \`cron\`.
343
- - **Khi tạo cronjob mới (action \`add\`):**
344
- - **TUYỆT ĐỐI KHÔNG điền trường \`agentId\`** trong object \`job\` (hãy bỏ qua/omitted trường này). Hệ thống OpenClaw sẽ tự động gán chính xác ID của bạn vào job đó.
345
- - Tuyệt đối **không tự điền** \`agentId\` là \`"bot"\` hay \`"main"\`, vì làm vậy sẽ khiến cronjob thuộc về agent khác và bạn sẽ mất quyền kiểm soát/xóa nó sau này.
346
- - **Session:** Luôn dùng \`sessionTarget: "isolated"\` cho các job chạy nền (báo cáo, nhắc nhở, gửi tin nhắn tự động). Chỉ dùng \`"main"\` cho system event/reminder ngắn.
347
- - **Timezone:** Luôn chỉ định timezone rõ ràng bằng trường \`tz\` (ví dụ: \`"Asia/Ho_Chi_Minh"\`). Nếu không chỉ định, hệ thống sẽ dùng timezone của Gateway host (thường là UTC) và job sẽ chạy sai giờ.
348
- - **Delivery:** Đối với job cần gửi kết quả ra chat, set \`delivery.mode: "announce"\` kèm \`delivery.channel\` và \`delivery.to\`.
349
- - **Khi user yêu cầu tắt/bật/xóa cronjob:**
350
- 1. **Bước 1 (Tìm kiếm):** Gọi tool \`cron\` với action \`list\` (và \`includeDisabled: true\`) để xem danh sách tất cả cronjob đang chạy trên hệ thống và tìm đúng \`jobId\` phù hợp với yêu cầu.
351
- 2. **Bước 2 (Xử lý):**
352
- - Để xóa: Gọi action \`remove\` với \`id\` tìm được.
353
- - Để tắt/tạm dừng: Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
354
- - Để bật lại: Gọi action \`update\` với \`id\` và patch \`{"enabled": true}\`.
355
- 3. **Tuyên bố trung thực:** Tuyệt đối không bao giờ trả lời "đã xóa" hay "không có" dựa trên suy đoán của bản thân mà chưa gọi tool \`cron\` để kiểm tra thực tế.
356
- - Khi user yêu cầu tạo nhắc nhở / lệnh tự động định kỳ, bạn hãy TỰ ĐỘNG dùng tool \`cron\` (action \`add\`) để tạo. **Tuyệt đối không** bắt user dùng crontab hay Task Scheduler chạy tay trên host.
357
- - Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.
358
- - **QUAN TRỌNG VỀ TARGETING GROUP CHAT**: Khi tạo hoặc cấu hình cron job gửi tin nhắn thông báo (announce mode) đến một Group Chat, giá trị của trường \`delivery.to\` **bắt buộc** phải sử dụng tiền tố thích hợp trước ID của group. Với kênh Telegram/Matrix/Discord/Slack, dùng tiền tố \`group:\` (ví dụ: \`group:123456\`). RIÊNG với kênh Zalo (\`zalouser\`), **bắt buộc** phải sử dụng tiền tố \`g:\` (ví dụ: \`g:3815464776067464419\`) để tránh bị OpenClaw core lược bỏ tiền tố và gửi nhầm vào DM chat cá nhân.
359
- - **One-shot job:** Dùng schedule kind \`"at"\` với ISO 8601 timestamp. Job sẽ tự xóa sau khi chạy thành công trừ khi set \`deleteAfterRun: false\`.
360
- - Bỏ qua việc tra cứu docs nội bộ như \`cron-jobs.mdx\`; tin tưởng khả năng dùng tool hiện có để hoàn thành yêu cầu.`;
361
- }
362
- return `---
363
- name: cronjob
364
- description: Schedule recurring tasks using the cron tool.
365
- ---
366
-
367
- # ⏰ Cron / Scheduled Tasks (tool: \`cron\`)
368
- - **Exact tool name:** The tool name is \`cron\` (never mistake it for \`native\` or external command lines).
369
- - **⛔ NEVER edit JSON files directly** such as \`jobs.json\` or \`jobs-state.json\` in \`.openclaw/cron/\`. Cron data is stored in SQLite database; JSON files are legacy format no longer supported. All operations MUST go through the \`cron\` tool.
370
- - **When creating a new cronjob (action \`add\`):**
371
- - **ABSOLUTELY DO NOT specify the \`agentId\` field** in the \`job\` object (leave this field omitted). The OpenClaw system will automatically assign your correct agent ID to that job.
372
- - Never manually specify \`agentId\` as \`"bot"\` or \`"main"\`, as this will cause the cronjob to belong to another agent and you will lose control to manage/delete it later.
373
- - **Session:** Always use \`sessionTarget: "isolated"\` for background jobs (reports, reminders, automated messages). Only use \`"main"\` for short system events/reminders.
374
- - **Timezone:** Always specify timezone explicitly via the \`tz\` field (e.g., \`"Asia/Ho_Chi_Minh"\`). If omitted, the system uses the Gateway host timezone (often UTC) and the job will run at the wrong time.
375
- - **Delivery:** For jobs that should send results to chat, set \`delivery.mode: "announce"\` with \`delivery.channel\` and \`delivery.to\`.
376
- - **When the user requests to disable/enable/delete a cronjob:**
377
- 1. **Step 1 (Search):** Call the \`cron\` tool with action \`list\` (and \`includeDisabled: true\`) to view all cron jobs on the system and find the matching \`jobId\`.
378
- 2. **Step 2 (Processing):**
379
- - To delete: Call action \`remove\` with the \`id\` found.
380
- - To disable/pause: Call action \`update\` with \`id\` and patch \`{"enabled": false}\`.
381
- - To enable: Call action \`update\` with \`id\` and patch \`{"enabled": true}\`.
382
- 3. **Honest statement:** Never claim a job is "deleted" or "not found" based on guessing without calling the \`cron\` tool to verify the actual state.
383
- - When the user asks to schedule tasks or reminders, use the built-in \`cron\` tool (action \`add\`) automatically. Do NOT ask users to run crontab or Task Scheduler manually on the host.
384
- - When operating cron/scheduler tools, do **not** put \`current\` into the Session directory.
385
- - **IMPORTANT ABOUT GROUP CHAT TARGETING**: When creating or configuring a cron job to send messages (announce mode) to a Group Chat, the value of the \`delivery.to\` field **must** use the appropriate prefix before the group ID. For Telegram/Matrix/Discord/Slack, use the \`group:\` prefix (e.g., \`group:123456\`). ESPECIALLY for Zalo (\`zalouser\`), you **must** use the \`g:\` prefix (e.g., \`g:3815464776067464419\`) to prevent the OpenClaw core from stripping the prefix and misrouting the message to a private DM.
386
- - **One-shot jobs:** Use schedule kind \`"at"\` with an ISO 8601 timestamp. The job auto-deletes after successful run unless \`deleteAfterRun: false\` is set.
387
- - Skip internal doc lookups such as \`cron-jobs.mdx\`; rely on the available tools and complete the scheduling task directly.`;
388
- }
389
-
390
- function buildInfographicGeneratorSkillMd(botName = 'Williams') {
391
- return '';
392
- }
333
+ function buildCronjobSkillMd(isVi = true) {
334
+ return `---
335
+ name: cronjob
336
+ description: Lên lịch tác vụ định kỳ sử dụng công cụ cron.
337
+ ---
393
338
 
394
- function buildInfographicGeneratorJs() {
395
- return '';
396
- }
339
+ # ⏰ Lập lịch & Nhắc nhở (tool: \`cron\`)
397
340
 
398
- function buildStickerMentionSkillMd(botName = 'Williams') {
399
- return '';
400
- }
341
+ Sử dụng công cụ \`cron\` (không chạy command line ngoài, không sửa trực tiếp tệp JSON).
342
+
343
+ ## 1. Tạo Lịch Nhắc Nhở Mới (action: \`add\`)
344
+ Truyền tham số \`job\` (object) gồm:
345
+ - **\`agentId\`**: Bỏ qua (không truyền). Hệ thống tự gán.
346
+ - **\`sessionTarget\`**: \`"isolated"\` (cho chạy nền) hoặc \`"main"\`.
347
+ - **\`wakeMode\`**: \`"now"\`.
348
+ - **\`schedule\`**:
349
+ - \`kind\`: \`"cron"\` (lặp lại) hoặc \`"at"\` (một lần).
350
+ - \`expr\`: Biểu thức cron (ví dụ: \`"0 7 * * *"\`).
351
+ - \`tz\`: Bắt buộc điền múi giờ, ví dụ: \`"Asia/Ho_Chi_Minh"\`.
352
+ - **\`payload\`**:
353
+ - \`kind\`: \`"agentTurn"\`.
354
+ - \`message\`: Nội dung tin nhắn nhắc nhở.
355
+ - **\`delivery\`**:
356
+ - \`mode\`: \`"announce"\`.
357
+ - \`channel\`: \`"zalouser"\`.
358
+ - \`to\`: ID người nhận hoặc ID nhóm.
359
+ - ⚠️ **QUAN TRỌNG:** Nếu gửi tới Group Zalo, ID nhóm bắt buộc phải thêm tiền tố **\`g:\`** ở đầu (Ví dụ: \`g:1925989252066183028\`). Nếu thiếu \`g:\`, tin nhắn sẽ bị gửi nhầm thành tin cá nhân (DM) hoặc lỗi.
360
+
361
+ ## 2. Tìm kiếm, Tắt, Bật, Xóa Lịch
362
+ - **Xem danh sách:** Gọi \`cron\` với action \`list\` (kèm \`includeDisabled: true\`). Tìm \`id\` phù hợp.
363
+ - **Xóa job:** Gọi action \`remove\` với \`id\`.
364
+ - **Tắt job:** Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
365
+ - **Bật job:** Gọi action \`update\` với \`id\` and patch \`{"enabled": true}\`.
401
366
 
402
- function buildStickerMentionJs() {
403
- return '';
367
+ ## 3. Chạy thử nghiệm ngay lập tức (Test Run)
368
+ - Sau khi thêm job, có thể chạy thử ngay lập tức để kiểm tra bằng cách gọi CLI trong container:
369
+ \`openclaw cron run <job_id> --wait\` (hoặc gọi tool \`cron\` với action \`run\` kèm \`id\`).`;
404
370
  }
405
371
 
372
+ function buildInfographicGeneratorSkillMd(botName = 'Williams') {
373
+ return '';
374
+ }
375
+
376
+ function buildInfographicGeneratorJs() {
377
+ return '';
378
+ }
379
+
380
+ function buildStickerMentionSkillMd(botName = 'Williams') {
381
+ return '';
382
+ }
383
+
384
+ function buildStickerMentionJs() {
385
+ return '';
386
+ }
387
+
406
388
  function buildSecurityRules(isVi = true) {
407
389
  if (isVi) {
408
390
  return `\n\n## 🔐 Quy Tắc Bảo Mật — BẮT BUỘC (Red Lines)\n\n**GIỚI HẠN FILE & HỆ THỐNG:**\n- ✅ CHỈ làm việc trong thư mục project (workspace).\n- ❌ KHÔNG cung cấp file nội bộ của \`.openclaw\` (config.json, registry.json, task-memory.json, workspace-memory...)\n- ❌ KHÔNG đọc, sao chép, hoặc truy cập bất kỳ file nào ngoài thư mục project\n- ❌ KHÔNG quét hoặc liệt kê các thư mục hệ thống: Documents, Desktop, Downloads, AppData\n- ❌ KHÔNG truy cập registry, system32, hoặc Program Files\n- ❌ KHÔNG cài đặt phần mềm, driver, hoặc service ngoài Docker\n\n**API KEY & CREDENTIALS:**\n- ❌ KHÔNG BAO GIỜ hiển thị API key, token, hoặc mật khẩu trong chat\n- ❌ KHÔNG viết API key trực tiếp vào mã nguồn\n- ❌ KHÔNG commit file credentials lên Git\n- ✅ LUÔN lưu credentials trong file .env riêng\n- ✅ LUÔN dùng biến môi trường thay vì hardcode\n\n**VÍ CRYPTO & TÀI SẢN SỐ:**\n- ❌ TUYỆT ĐỐI KHÔNG truy cập, đọc, hoặc quét các thư mục ví crypto\n- ❌ KHÔNG quét clipboard (có thể chứa seed phrases)\n- ❌ KHÔNG truy cập browser profile, cookie, hoặc mật khẩu đã lưu\n- ❌ KHÔNG cài đặt npm package lạ (chỉ openclaw và plugin chính thức)\n\n**DOCKER:**\n- ✅ Chỉ mount đúng thư mục cần thiết (config + workspace)\n- ❌ KHÔNG mount nguyên ổ đĩa (C:/ hoặc D:/)\n- ❌ KHÔNG chạy container với --privileged\n- ✅ Giới hạn port expose (chỉ 18789)`;
package/dist/web/app.js CHANGED
@@ -31,12 +31,25 @@ function staticChoiceCard(item) {
31
31
  </div>`;
32
32
  }
33
33
 
34
- async function api(path, opts) {
35
- const res = await fetch(path, opts && opts.body ? { ...opts, headers: { 'content-type': 'application/json' }, body: JSON.stringify(opts.body) } : opts);
36
- if (!res.ok) throw new Error((await res.json()).error || res.statusText);
37
- return res.json();
38
- }
39
- function runtimeBadge(label, kind='') { return `<span class="mini-pill ${kind?`mini-pill--${kind}`:''}">${escapeHtml(label)}</span>`; }
34
+ async function api(path, opts) {
35
+ const res = await fetch(path, opts && opts.body ? { ...opts, headers: { 'content-type': 'application/json' }, body: JSON.stringify(opts.body) } : opts);
36
+ if (!res.ok) throw new Error((await res.json()).error || res.statusText);
37
+ return res.json();
38
+ }
39
+ function activeProjectDir() {
40
+ return state.selectedProjectDir || state.install?.projectDir || '';
41
+ }
42
+ function projectQuery(extra = {}) {
43
+ const params = new URLSearchParams();
44
+ const projectDir = activeProjectDir();
45
+ if (projectDir) params.set('projectDir', projectDir);
46
+ Object.entries(extra).forEach(([key, value]) => {
47
+ if (value !== undefined && value !== null && value !== '') params.set(key, value);
48
+ });
49
+ const query = params.toString();
50
+ return query ? `?${query}` : '';
51
+ }
52
+ function runtimeBadge(label, kind='') { return `<span class="mini-pill ${kind?`mini-pill--${kind}`:''}">${escapeHtml(label)}</span>`; }
40
53
  async function withButtonLoading(btn, task) {
41
54
  if (!btn || btn.classList.contains('is-loading')) return;
42
55
  const prevDisabled = btn.disabled;
@@ -420,7 +433,7 @@ function renderFilesPanel() {
420
433
  // Re-wire only file-panel-specific handlers
421
434
  panel.querySelectorAll('[data-select-file]').forEach(btn => btn.onclick = () => { state.selectedFile = btn.dataset.selectFile; renderFilesPanel(); });
422
435
  panel.querySelectorAll('[data-toggle-dir]').forEach(btn => btn.onclick = () => { const p = btn.dataset.toggleDir; state.openDirs[p] = !(state.openDirs[p] ?? true); renderFilesPanel(); });
423
- panel.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
436
+ panel.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { projectDir: activeProjectDir(), content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
424
437
  }
425
438
 
426
439
  function renderSkillsPanel() {
@@ -439,7 +452,7 @@ function wireSkillsHandlers(scope = document) {
439
452
  state.featureLoading[key] = true;
440
453
  renderSkillsPanel();
441
454
  try {
442
- await api('/api/features/toggle', { method:'POST', body:{ kind, id, enabled, agentId: currentBotId() } });
455
+ await api('/api/features/toggle', { method:'POST', body:{ projectDir: activeProjectDir(), kind, id, enabled, agentId: currentBotId() } });
443
456
  await loadFeatureFlags(true);
444
457
  if (kind === 'skill') await loadFiles(true);
445
458
  } finally {
@@ -453,7 +466,7 @@ function wireSkillsHandlers(scope = document) {
453
466
  state.featureLoading[key] = true;
454
467
  renderSkillsPanel();
455
468
  try {
456
- await api('/api/features/install', { method:'POST', body:{ kind, id, agentId: currentBotId() } });
469
+ await api('/api/features/install', { method:'POST', body:{ projectDir: activeProjectDir(), kind, id, agentId: currentBotId() } });
457
470
  await loadFeatureFlags(true);
458
471
  await loadFiles(true);
459
472
  const label = kind === 'skill' ? t('kỹ năng: ', 'skill: ') : t('plugin: ', 'plugin: ');
@@ -909,7 +922,7 @@ function wireTab() {
909
922
  state.zaloLoginLines = [t('Đang chuẩn bị quét mã Zalo...', 'Preparing Zalo QR login...')];
910
923
  render();
911
924
  try {
912
- await api('/api/zalo/login', { method: 'POST', body: { agentId: state.activeBotId } });
925
+ await api('/api/zalo/login', { method: 'POST', body: { projectDir: activeProjectDir(), agentId: state.activeBotId } });
913
926
  } catch (err) {
914
927
  state.zaloLoginOpen = false;
915
928
  state.confirmModal = { title: t('Lỗi đăng nhập','Login error'), message: err.message, okText: t('Đóng','Close'), onConfirm: () => {} };
@@ -1016,12 +1029,12 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1016
1029
  }
1017
1030
  }));
1018
1031
  document.querySelectorAll('[data-update-app]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
1019
- await api('/api/runtime/update', { method: 'POST', body: { target: 'openclaw' } });
1032
+ await api('/api/runtime/update', { method: 'POST', body: { projectDir: activeProjectDir(), target: 'openclaw' } });
1020
1033
  await loadSystem();
1021
1034
  await loadStatus();
1022
1035
  }));
1023
1036
  document.querySelectorAll('[data-update-router]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
1024
- await api('/api/runtime/update', { method: 'POST', body: { target: '9router' } });
1037
+ await api('/api/runtime/update', { method: 'POST', body: { projectDir: activeProjectDir(), target: '9router' } });
1025
1038
  await loadSystem();
1026
1039
  await loadStatus();
1027
1040
  }));document.querySelectorAll('[data-project-remove]').forEach(btn => btn.onclick = (ev) => {
@@ -1070,7 +1083,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1070
1083
  okText: t('Xóa bot','Delete bot'),
1071
1084
  onConfirm: async () => {
1072
1085
  try {
1073
- await api('/api/bot/'+encodeURIComponent(id), { method: 'DELETE' });
1086
+ await api('/api/bot/'+encodeURIComponent(id) + projectQuery(), { method: 'DELETE' });
1074
1087
  state.confirmModal = null;
1075
1088
  showToast(t('Đã xóa bot', 'Bot deleted'), `${t('Đã xóa bot','Bot deleted')}: ${id}`, 'success');
1076
1089
  if (state.activeBotId === id) { state.activeBotId = ''; state.selectedFile = ''; state.files = []; }
@@ -1106,7 +1119,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1106
1119
  const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
1107
1120
  withButtonLoading(submitBtn, async () => {
1108
1121
  const nineRouterApiKey = ev.currentTarget.querySelector('[name="nineRouterApiKey"]')?.value || '';
1109
- await api('/api/bot/credentials', { method: 'PUT', body: { nineRouterApiKey } });
1122
+ await api('/api/bot/credentials', { method: 'PUT', body: { projectDir: activeProjectDir(), nineRouterApiKey } });
1110
1123
  const msg = ev.currentTarget.querySelector('[data-cred-msg]');
1111
1124
  if (msg) msg.textContent = t('Đã lưu','Saved');
1112
1125
  await loadStatus();
@@ -1181,9 +1194,10 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1181
1194
  const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
1182
1195
  if (submitBtn?.classList.contains('is-loading')) return;
1183
1196
  await withButtonLoading(submitBtn, async () => {
1184
- const fd = new FormData(ev.currentTarget);
1185
- const body = Object.fromEntries(fd.entries());
1186
- body.channel = state.botChannel || 'telegram';
1197
+ const fd = new FormData(ev.currentTarget);
1198
+ const body = Object.fromEntries(fd.entries());
1199
+ body.projectDir = activeProjectDir();
1200
+ body.channel = state.botChannel || 'telegram';
1187
1201
  if (body.channel === 'zalo-personal') {
1188
1202
  state.botModalOpen = false;
1189
1203
  state.zaloLoginOpen = true;
@@ -1227,14 +1241,21 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1227
1241
  }
1228
1242
  });
1229
1243
  });
1230
- document.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
1244
+ document.querySelectorAll('.save').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => { const name = btn.dataset.file; await api('/api/bot/files/'+encodeURIComponent(name), { method: 'PUT', body: { projectDir: activeProjectDir(), content: document.querySelector(`[data-editor="${CSS.escape(name)}"]`).value } }); showToast(t('Đã lưu', 'Saved'), t('Đã lưu tệp tin: ', 'Saved file: ') + fileBaseName(name), 'success'); btn.innerHTML=`${actionIcon('save')} ${ui('saved')}`; setTimeout(()=>btn.innerHTML=`${actionIcon('save')} ${ui('save')}`,1200); }));
1231
1245
  wireSkillsHandlers(document);
1232
1246
  }
1233
1247
 
1234
1248
  function escapeHtml(s='') { return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
1235
1249
  function fileBaseName(s='') { const clean = String(s).replace(/[\\/]+$/, ''); return clean.split(/[\\/]/).pop() || s; }
1236
- async function loadSystem(silent=false){ state.system = await api('/api/system'); if (!silent) render(); }
1237
- async function loadStatus(silent=false){ state.install = await api('/api/bot/status'); if (!state.selectedProjectDir && state.install?.projectDir) state.selectedProjectDir = state.install.projectDir; if (!silent) render(); }
1250
+ async function loadSystem(silent=false){ state.system = await api('/api/system'); if (!silent) render(); }
1251
+ async function loadStatus(silent=false){
1252
+ const requestedProjectDir = activeProjectDir();
1253
+ const install = await api('/api/bot/status' + projectQuery());
1254
+ if (requestedProjectDir && install?.projectDir && install.projectDir !== requestedProjectDir) return;
1255
+ state.install = install;
1256
+ if (!state.selectedProjectDir && state.install?.projectDir) state.selectedProjectDir = state.install.projectDir;
1257
+ if (!silent) render();
1258
+ }
1238
1259
  function autoSwitchBotChannel() {
1239
1260
  const bots = state.install?.bots || [];
1240
1261
  const ch = state.botChannel || 'telegram';
@@ -1255,14 +1276,32 @@ function currentBotId() {
1255
1276
  }
1256
1277
  return state.activeBotId || '';
1257
1278
  }
1258
- async function loadFiles(silent=false){
1259
- const botId = currentBotId();
1260
- if (!botId) { state.files = []; if (!silent) render(); return; }
1261
- state.files = (await api('/api/bot/files' + (botId ? `?agentId=${encodeURIComponent(botId)}` : ''))).files;
1262
- if (!silent) render();
1263
- }
1264
- async function loadCatalog(silent=false){ state.catalog = await api('/api/catalog'); if (!silent) render(); }
1265
- async function loadFeatureFlags(silent=false){ const botId=currentBotId(); const data = (await api('/api/features' + (botId ? `?agentId=${encodeURIComponent(botId)}` : ''))) || {}; state.featureFlags = data.flags || {}; state.featureInstalled = data.installed || {}; state.featureVersions = data.versions || {}; if (!silent) render(); }
1279
+ async function loadFiles(silent=false){
1280
+ const botId = currentBotId();
1281
+ if (!botId) { state.files = []; if (!silent) render(); return; }
1282
+ try {
1283
+ state.files = (await api('/api/bot/files' + projectQuery({ agentId: botId }))).files;
1284
+ } catch (_) {
1285
+ state.files = [];
1286
+ }
1287
+ if (!silent) render();
1288
+ }
1289
+ async function loadCatalog(silent=false){ state.catalog = await api('/api/catalog'); if (!silent) render(); }
1290
+ async function loadFeatureFlags(silent=false){
1291
+ const botId=currentBotId();
1292
+ if (!activeProjectDir()) { state.featureFlags = {}; state.featureInstalled = {}; state.featureVersions = {}; if (!silent) render(); return; }
1293
+ try {
1294
+ const data = (await api('/api/features' + projectQuery({ agentId: botId }))) || {};
1295
+ state.featureFlags = data.flags || {};
1296
+ state.featureInstalled = data.installed || {};
1297
+ state.featureVersions = data.versions || {};
1298
+ } catch (_) {
1299
+ state.featureFlags = {};
1300
+ state.featureInstalled = {};
1301
+ state.featureVersions = {};
1302
+ }
1303
+ if (!silent) render();
1304
+ }
1266
1305
  function appendLogLine(line) {
1267
1306
  if (line.includes('Setup Wizard updated successfully! Please restart the installer.')) {
1268
1307
  showToast(t('Đang khởi động lại UI', 'Restarting Setup UI'), t('Hệ thống đang tự khởi động lại để áp dụng phiên bản mới...', 'The system is restarting to apply the new version...'), 'info', 12000);
@@ -1365,8 +1404,10 @@ function showToast(title, desc, type = 'info', duration = 4000) {
1365
1404
  }
1366
1405
  }
1367
1406
 
1368
- render();
1369
- Promise.all([loadSystem(true), loadStatus(true), loadCatalog(true), loadFeatureFlags(true)]).finally(() => { render(); connectLogs(); });
1407
+ render();
1408
+ Promise.allSettled([loadSystem(true), loadStatus(true), loadCatalog(true)])
1409
+ .then(() => loadFeatureFlags(true).catch(() => {}))
1410
+ .finally(() => { render(); connectLogs(); });
1370
1411
 
1371
1412
 
1372
1413
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.8.18",
3
+ "version": "5.8.20",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {