create-openclaw-bot 5.8.0 → 5.8.2

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.
@@ -21,6 +21,7 @@ const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
21
21
 
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
23
  const WEB_DIR = resolve(__dirname, '../web');
24
+ const SETUP_VERSION = (() => { try { return JSON.parse(fs.readFileSync(resolve(__dirname, '../../package.json'), 'utf8')).version || '0.0.0'; } catch { return '0.0.0'; } })();
24
25
  const DEFAULT_PROJECT_NAME = 'openclaw-bot';
25
26
  const STATE_FILE = '.openclaw-setup-state.json';
26
27
  const DEFAULT_MODEL = 'smart-route';
@@ -410,6 +411,12 @@ async function syncRuntimeState(projectDir) {
410
411
  state.mode = state.mode || rt.mode;
411
412
  state.syncSource = rt.syncSource || 'config';
412
413
  state.installed = true;
414
+ // Auto-sync Docker files if outdated
415
+ if (rt.mode === 'docker') {
416
+ await syncDockerInfra(projectDir).catch((err) =>
417
+ sendLog(`[sync] Docker infra sync skipped: ${err.message}`)
418
+ );
419
+ }
413
420
  }
414
421
 
415
422
  function uniqueSlug(base, used) {
@@ -1321,6 +1328,74 @@ function getBotContainerName(projectDir) {
1321
1328
  return 'openclaw-bot';
1322
1329
  }
1323
1330
 
1331
+ async function syncDockerInfra(projectDir, force = false) {
1332
+ const dockerDir = join(projectDir, 'docker', 'openclaw');
1333
+ if (!existsSync(join(dockerDir, 'docker-compose.yml'))) return false;
1334
+
1335
+ // Check existing entrypoint version stamp
1336
+ const entrypointPath = join(dockerDir, 'entrypoint.sh');
1337
+ const existingEntrypoint = existsSync(entrypointPath)
1338
+ ? await fsp.readFile(entrypointPath, 'utf8').catch(() => '') : '';
1339
+ const existingVersion = (existingEntrypoint.match(/# openclaw-setup v([\d.]+)/) || [])[1] || '0.0.0';
1340
+
1341
+ // Only regenerate if version differs OR force is true
1342
+ if (existingVersion === SETUP_VERSION && !force) return false;
1343
+
1344
+ // Read existing compose to preserve customizations
1345
+ const compose = await readComposeText(projectDir);
1346
+ const botContainer = parseComposeServiceContainerName(compose, 'ai-bot') || `openclaw-${slugify(basename(projectDir))}`;
1347
+ const routerContainer = parseComposeServiceContainerName(compose, '9router') || `9router-${slugify(basename(projectDir))}`;
1348
+ const composeName = (compose.match(/^name:\s*(\S+)/m) || [])[1] || `oc-${slugify(basename(projectDir))}`;
1349
+ const gatewayPort = state.gatewayPort || 18789;
1350
+ const routerPort = state.routerPort || 20128;
1351
+
1352
+ // Detect features from openclaw.json
1353
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
1354
+ let hasZalo = false;
1355
+ try {
1356
+ const cfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
1357
+ hasZalo = !!cfg.channels?.zalouser?.enabled;
1358
+ } catch {}
1359
+
1360
+ // Regenerate with detected settings
1361
+ const docker = buildDockerArtifacts({
1362
+ is9Router: true,
1363
+ openClawNpmSpec: OPENCLAW_NPM_SPEC,
1364
+ openClawRuntimePackages: '',
1365
+ allSkills: [],
1366
+ dockerfilePlugins: [],
1367
+ gatewayPort,
1368
+ routerPort,
1369
+ singleComposeName: composeName,
1370
+ singleAppContainerName: botContainer,
1371
+ singleRouterContainerName: routerContainer,
1372
+ runtimeCommandParts: [
1373
+ hasZalo ? 'ensure_zalouser' : '',
1374
+ 'while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done >/dev/null 2>&1 &',
1375
+ ].filter(Boolean),
1376
+ plainSingleExtraHosts: true,
1377
+ });
1378
+
1379
+ // Inject version stamp into entrypoint
1380
+ let entryScript = docker.entrypointScript || '';
1381
+ entryScript = entryScript.replace('#!/bin/sh', `#!/bin/sh\n# openclaw-setup v${SETUP_VERSION}`);
1382
+
1383
+ // Write updated files preserving env_file path convention
1384
+ const newCompose = String(docker.compose || '')
1385
+ .replace(/env_file:\s*\n\s*-\s*\.env/g, 'env_file:\n - ../../.env')
1386
+ .replace(/env_file:\s*\.env/g, 'env_file: ../../.env');
1387
+
1388
+ sendLog(`[sync] Updating Docker infrastructure files (v${existingVersion} \u2192 v${SETUP_VERSION})`);
1389
+ await fsp.writeFile(join(dockerDir, 'Dockerfile'), docker.dockerfile, 'utf8');
1390
+ await fsp.writeFile(join(dockerDir, 'docker-compose.yml'), newCompose, 'utf8');
1391
+ await fsp.writeFile(entrypointPath, entryScript, 'utf8');
1392
+ if (docker.syncScript) await fsp.writeFile(join(dockerDir, 'sync.js'), docker.syncScript, 'utf8');
1393
+ if (docker.patchScript) await fsp.writeFile(join(dockerDir, 'patch-9router.js'), docker.patchScript, 'utf8');
1394
+
1395
+ sendLog(`[sync] Docker files updated to v${SETUP_VERSION}. Next rebuild will use new infrastructure.`);
1396
+ return true;
1397
+ }
1398
+
1324
1399
  async function recreateDockerBot(projectDir) {
1325
1400
  const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
1326
1401
  if (!existsSync(composeFile)) return false;
@@ -1329,7 +1404,7 @@ async function recreateDockerBot(projectDir) {
1329
1404
  const serviceName = getBotServiceName(projectDir);
1330
1405
  const containerName = getBotContainerName(projectDir);
1331
1406
  sendLog(`[docker] Recreating ${serviceName} to reload openclaw.json/.env...`);
1332
- await run('docker', ['compose', '-f', composeFile, 'up', '-d', '--force-recreate', serviceName], { cwd: projectDir });
1407
+ await run('docker', ['compose', '-f', composeFile, 'up', '-d', '--build', '--force-recreate', serviceName], { cwd: projectDir });
1333
1408
  await waitForDockerContainer(containerName);
1334
1409
  return true;
1335
1410
  }
@@ -1343,6 +1418,8 @@ async function updateRuntime(target, projectDir) {
1343
1418
  await run('docker', ['compose', 'pull', '9router'], { cwd: dockerDir }).catch(() => {});
1344
1419
  await run('docker', ['compose', 'up', '-d', '--force-recreate', '9router'], { cwd: dockerDir });
1345
1420
  } else {
1421
+ // Ensure Docker files are current before rebuilding
1422
+ await syncDockerInfra(projectDir).catch(() => {});
1346
1423
  const serviceName = getBotServiceName(projectDir);
1347
1424
  const containerName = getBotContainerName(projectDir);
1348
1425
  await run('docker', ['compose', 'build', '--no-cache', serviceName], { cwd: dockerDir });
@@ -1443,7 +1520,8 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
1443
1520
  sendLog(`[writeCoreProject] Writing docker files to ${dockerDir} (compose ${compose.length} bytes, routerPort=${routerPort})`);
1444
1521
  await fsp.writeFile(join(dockerDir, 'Dockerfile'), docker.dockerfile, 'utf8');
1445
1522
  await fsp.writeFile(join(dockerDir, 'docker-compose.yml'), compose, 'utf8');
1446
- await fsp.writeFile(join(dockerDir, 'entrypoint.sh'), docker.entrypointScript || docker.entrypoint || '', 'utf8');
1523
+ const entryScript = (docker.entrypointScript || docker.entrypoint || '').replace('#!/bin/sh', `#!/bin/sh\n# openclaw-setup v${SETUP_VERSION}`);
1524
+ await fsp.writeFile(join(dockerDir, 'entrypoint.sh'), entryScript, 'utf8');
1447
1525
  // Write 9router helper scripts as separate files (mounted as volumes)
1448
1526
  if (docker.syncScript) await fsp.writeFile(join(dockerDir, 'sync.js'), docker.syncScript, 'utf8');
1449
1527
  if (docker.patchScript) await fsp.writeFile(join(dockerDir, 'patch-9router.js'), docker.patchScript, 'utf8');
@@ -1825,43 +1903,125 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
1825
1903
  defaultProfile: 'host-chrome',
1826
1904
  profiles: { 'host-chrome': { cdpUrl: 'http://127.0.0.1:9222', color: '#4285F4' } },
1827
1905
  };
1828
- const wm = buildWorkspaceFileMap({ isVi: true, botName: agent.name || agent.id, botDesc: '', hasBrowser: true, hasScheduler: true, workspacePath: `.openclaw/${workspaceRelForAgent(agent, cfg, projectDir)}/`, agentWorkspaceDir: workspaceRelForAgent(agent, cfg, projectDir), variant: 'single' });
1829
- const browserDoc = wm['BROWSER.md'] || '# BROWSER';
1830
- const browserTool = wm['browser-tool.js'] || '';
1831
- const bf = await readWorkspaceText(projectDir, agent, 'BROWSER.md');
1832
- await fsp.writeFile(bf.file, browserDoc, 'utf8');
1833
- const bt = await readWorkspaceText(projectDir, agent, 'browser-tool.js');
1834
- if (browserTool) await fsp.writeFile(bt.file, browserTool, 'utf8');
1835
- const af = await readWorkspaceText(projectDir, agent, 'AGENTS.md');
1836
- const agentsManaged = upsertManagedBlock(af.content, 'BROWSER_LINK', '- Browser docs: `BROWSER.md`');
1837
- await fsp.writeFile(af.file, agentsManaged, 'utf8');
1906
+ const isHeadlessServer = process.platform === 'linux';
1907
+ const docVariant = 'cli-server';
1908
+
1909
+ for (const a of cfg.agents.list) {
1910
+ const wm = buildWorkspaceFileMap({
1911
+ isVi: true,
1912
+ botName: a.name || a.id,
1913
+ botDesc: '',
1914
+ hasBrowser: false,
1915
+ hasScheduler: true,
1916
+ workspacePath: `.openclaw/${workspaceRelForAgent(a, cfg, projectDir)}/`,
1917
+ agentWorkspaceDir: workspaceRelForAgent(a, cfg, projectDir),
1918
+ variant: cfg.agents.list.length > 1 ? 'relay' : 'single',
1919
+ browserDocVariant: docVariant,
1920
+ });
1921
+ const browserDoc = wm['BROWSER.md'] || '# BROWSER';
1922
+ const browserTool = wm['browser-tool.js'] || '';
1923
+ const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
1924
+ await fsp.writeFile(bf.file, browserDoc, 'utf8');
1925
+ const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
1926
+ if (browserTool) await fsp.writeFile(bt.file, browserTool, 'utf8');
1927
+
1928
+ const af = await readWorkspaceText(projectDir, a, 'AGENTS.md');
1929
+ const agentsManaged = upsertManagedBlock(af.content, 'BROWSER_LINK', '- Browser docs: `BROWSER.md`');
1930
+ await fsp.writeFile(af.file, agentsManaged, 'utf8');
1931
+
1932
+ // Add to TOOLS.md
1933
+ const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
1934
+ const browserGuide = isHeadlessServer
1935
+ ? `## 🌐 Browser Automation
1936
+ - Xem hướng dẫn chi tiết tại **BROWSER.md**
1937
+ - Script điều khiển: \`browser-tool.js\`
1938
+ - Chế độ hiện tại: Chạy ngầm độc lập qua Docker hoặc Xvfb trên VPS.
1939
+ - **Tìm kiếm Web:** Nếu không có công cụ Web Search (hoặc Web Search không khả dụng/bị lỗi), hãy **luôn sử dụng ngay công cụ terminal (exec/run_command) để chạy lệnh: \`node search-tool.js "<từ khóa>" 5\`**! Lệnh này sẽ tự động chạy ngầm qua DuckDuckGo/Google/Bing bằng trình duyệt ngầm tàng hình của bạn và trả về kết quả JSON sạch ngay lập tức. Tuyệt đối KHÔNG được mở trình duyệt thủ công, chụp ảnh màn hình hay click tìm kiếm bằng tay từng bước!
1940
+ - Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`
1941
+ : `## 🌐 Browser Automation
1942
+ - Xem hướng dẫn chi tiết tại **BROWSER.md**
1943
+ - Script điều khiển: \`browser-tool.js\`
1944
+ - Chế độ hiện tại:
1945
+ - **Mặc định:** Chạy ngầm độc lập qua Docker hoặc Server.
1946
+ - **Chế độ xem Chrome thật:** Chạy file \`start-chrome-debug.bat\` / \`start-chrome-debug.sh\` trên host trước để bot kết nối điều khiển trực quan.
1947
+ - Kết nối mặc định: \`http://127.0.0.1:9222\`
1948
+ - **Tìm kiếm Web:** Nếu không có công cụ Web Search (hoặc Web Search không khả dụng/bị lỗi), hãy **luôn sử dụng ngay công cụ terminal (exec/run_command) để chạy lệnh: \`node search-tool.js "<từ khóa>" 5\`**! Lệnh này sẽ tự động chạy ngầm qua DuckDuckGo/Google/Bing bằng trình duyệt ngầm tàng hình của bạn và trả về kết quả JSON sạch ngay lập tức. Tuyệt đối KHÔNG được mở trình duyệt thủ công, chụp ảnh màn hình hay click tìm kiếm bằng tay từng bước!
1949
+ - Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`;
1950
+ await fsp.writeFile(tf.file, upsertManagedBlock(tf.content, 'BROWSER_GUIDE', browserGuide), 'utf8');
1951
+ }
1838
1952
  } else {
1839
1953
  delete cfg.browser;
1840
- const bf = await readWorkspaceText(projectDir, agent, 'BROWSER.md');
1841
- if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
1842
- const bt = await readWorkspaceText(projectDir, agent, 'browser-tool.js');
1843
- if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
1844
- const af = await readWorkspaceText(projectDir, agent, 'AGENTS.md');
1845
- await fsp.writeFile(af.file, removeManagedBlock(af.content, 'BROWSER_LINK'), 'utf8');
1954
+ for (const a of cfg.agents.list) {
1955
+ const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
1956
+ if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
1957
+ const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
1958
+ if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
1959
+
1960
+ const af = await readWorkspaceText(projectDir, a, 'AGENTS.md');
1961
+ await fsp.writeFile(af.file, removeManagedBlock(af.content, 'BROWSER_LINK'), 'utf8');
1962
+
1963
+ // Remove from TOOLS.md
1964
+ const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
1965
+ await fsp.writeFile(tf.file, removeManagedBlock(tf.content, 'BROWSER_GUIDE'), 'utf8');
1966
+ }
1967
+ }
1968
+
1969
+ // Write cfgPath early so syncDockerInfra reads the updated openclaw.json
1970
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
1971
+
1972
+ // Force Docker Infrastructure sync and container recreation
1973
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
1974
+ if (hasDocker) {
1975
+ sendLog(`[docker] Browser skill toggled to ${enabled}. Regenerating Dockerfiles...`);
1976
+ await syncDockerInfra(projectDir, true).catch((err) => sendLog(`[docker] Warning: Failed to sync docker infra: ${err.message}`));
1977
+ sendLog(`[docker] Rebuilding and recreating containers...`);
1978
+ await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
1846
1979
  }
1847
1980
  }
1848
1981
 
1849
1982
  if (kind === 'skill' && id === 'cron') {
1850
- const tf = await readWorkspaceText(projectDir, agent, 'TOOLS.md');
1851
1983
  if (enabled) {
1852
1984
  cfg.tools = cfg.tools || { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
1853
1985
  cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:automation']));
1854
1986
  cfg.commands = cfg.commands || {};
1855
1987
  cfg.commands.ownerAllowFrom = Array.from(new Set([...(cfg.commands.ownerAllowFrom || []), '*']));
1856
- const cronGuide = `## Cron (OpenClaw)
1857
- - Dùng tool scheduler/cron native của OpenClaw để tạo job định kỳ.
1858
- - Không yêu cầu user tự chạy crontab/Task Scheduler trên hos t.
1859
- - Luôn xác nhận: lịch, múi giờ, nội dung job trước khi lưu.`;
1860
- await fsp.writeFile(tf.file, upsertManagedBlock(tf.content, 'CRON_GUIDE', cronGuide), 'utf8');
1988
+ const cronGuide = `## Cron / Lên lịch nhắc nhở (tool: \`cron\`)
1989
+ - **Tên tool chính xác:** Tên công cụ là \`cron\` (tuyệt đối không nhầm \`native\` hay command line bên ngoài).
1990
+ - **Khi tạo cronjob mới (action \`add\`):**
1991
+ - **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 đó.
1992
+ - 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.
1993
+ - **Khi user yêu cầu tắt/bật/xóa cronjob:**
1994
+ 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.
1995
+ 2. **Bước 2 (Xử lý):**
1996
+ - Để xóa: Gọi action \`remove\` với \`id\` tìm được.
1997
+ - Để tắt/tạm dừng: Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
1998
+ - Để bật lại: Gọi action \`update\` với \`id\` và patch \`{"enabled": true}\`.
1999
+ 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ế.
2000
+ - 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.
2001
+ - Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.
2002
+ - **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ố \`group:\` trước ID của group (ví dụ: \`group:3815464776067464419\` hoặc \`group:xxxx\`). Tuyệt đối không được chỉ điền ID thuần túy vì hệ thống sẽ hiểu nhầm đó là một DM chat cá nhân (direct message) và gửi sai địa chỉ.
2003
+ - 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.`;
2004
+ for (const a of cfg.agents.list) {
2005
+ const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
2006
+ await fsp.writeFile(tf.file, upsertManagedBlock(tf.content, 'CRON_GUIDE', cronGuide), 'utf8');
2007
+ }
1861
2008
  } else {
1862
2009
  if (cfg.tools?.alsoAllow) cfg.tools.alsoAllow = cfg.tools.alsoAllow.filter((x) => x !== 'group:automation');
1863
2010
  if (cfg.commands?.ownerAllowFrom) cfg.commands.ownerAllowFrom = cfg.commands.ownerAllowFrom.filter((x) => x !== '*');
1864
- await fsp.writeFile(tf.file, removeManagedBlock(tf.content, 'CRON_GUIDE'), 'utf8');
2011
+ for (const a of cfg.agents.list) {
2012
+ const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
2013
+ await fsp.writeFile(tf.file, removeManagedBlock(tf.content, 'CRON_GUIDE'), 'utf8');
2014
+ }
2015
+ }
2016
+
2017
+ // Write cfgPath early so recreation reads updated openclaw.json
2018
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2019
+
2020
+ // Recreate container to apply updated openclaw.json tools/commands rules
2021
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2022
+ if (hasDocker) {
2023
+ sendLog(`[docker] Cron skill toggled to ${enabled}. Recreating containers...`);
2024
+ await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
1865
2025
  }
1866
2026
  }
1867
2027
 
@@ -1869,9 +2029,10 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
1869
2029
  cfg.plugins = cfg.plugins || { entries: {} };
1870
2030
  cfg.plugins.entries = cfg.plugins.entries || {};
1871
2031
  const pluginAliasMap = {
2032
+ 'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
1872
2033
  'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
1873
- 'openclaw-n8n-facebook-crawler': ['openclaw-n8n-facebook-crawler', 'openclaw-facebook-crawler', 'n8n-facebook-crawler'],
1874
- 'openclaw-facebook-poster': ['openclaw-facebook-poster', 'openclaw-n8n-facebook-poster', 'facebook-poster'],
2034
+ 'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
2035
+ 'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
1875
2036
  };
1876
2037
  const aliases = pluginAliasMap[id] || [id];
1877
2038
  const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
@@ -1881,12 +2042,9 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
1881
2042
  cfg.plugins.entries[existingKey].hooks = cfg.plugins.entries[existingKey].hooks || {};
1882
2043
  cfg.plugins.entries[existingKey].hooks.allowConversationAccess = true;
1883
2044
  }
1884
- // Ensure plugins.allow includes this plugin so gateway loads it
2045
+ // Only add the canonical config key to allow list (not all aliases)
1885
2046
  cfg.plugins.allow = cfg.plugins.allow || [];
1886
- const allowIds = aliases.concat(existingKey);
1887
- for (const aid of allowIds) {
1888
- if (!cfg.plugins.allow.includes(aid)) cfg.plugins.allow.push(aid);
1889
- }
2047
+ if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
1890
2048
  }
1891
2049
 
1892
2050
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
@@ -1904,33 +2062,76 @@ async function installFeature(projectDir, agentId, kind, id) {
1904
2062
 
1905
2063
  if (composeDir) {
1906
2064
  const botContainer = getBotContainerName(projectDir);
2065
+
2066
+ // 1. Temporarily disable the plugin in openclaw.json and restart container to unlock files
2067
+ const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
2068
+ const pluginAliasMap = {
2069
+ 'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
2070
+ 'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
2071
+ 'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
2072
+ 'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
2073
+ };
2074
+ const aliases = pluginAliasMap[id] || [id];
2075
+
2076
+ if (existsSync(cfgPath)) {
2077
+ try {
2078
+ const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
2079
+ cfg.plugins = cfg.plugins || { entries: {} };
2080
+ cfg.plugins.entries = cfg.plugins.entries || {};
2081
+ const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
2082
+
2083
+ if (cfg.plugins.entries[existingKey]?.enabled) {
2084
+ sendLog(`[plugin] Temporarily disabling ${existingKey} and restarting bot to release file locks...`);
2085
+ cfg.plugins.entries[existingKey].enabled = false;
2086
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2087
+ await run('docker', ['restart', botContainer], { shell: false }).catch(() => {});
2088
+ // Sleep 2 seconds to let container fully boot and release locks
2089
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2090
+ }
2091
+ } catch (_) {}
2092
+ }
2093
+
1907
2094
  sendLog(`[plugin] Installing clawhub:${id} inside container ${botContainer}...`);
1908
2095
 
1909
2096
  let installSuccess = true;
1910
- const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', `cd /root/project && openclaw plugins install clawhub:${id}`], { cwd: projectDir, shell: false }).catch((err) => {
1911
- const aliases = ['openclaw-zalo-mod', 'zalo-mod', id, id.replace('openclaw-', '')];
2097
+ const cleanCmd = `cd /root/project && (openclaw plugins uninstall ${id} --force 2>/dev/null || true) && (openclaw plugins uninstall ${id.replace('openclaw-', '')} --force 2>/dev/null || true) && rm -rf .openclaw/extensions/${id} .openclaw/extensions/${id.replace('openclaw-', '')} && openclaw plugins install clawhub:${id}`;
2098
+ const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cleanCmd], { cwd: projectDir, shell: false });
2099
+
2100
+ if (cmdOut) {
2101
+ for (const line of `${cmdOut.stdout}\n${cmdOut.stderr}`.split(/\r?\n/).filter(Boolean)) sendLog(line);
2102
+ }
2103
+
2104
+ if (cmdOut.code !== 0) {
1912
2105
  const folderExists = aliases.some((a) => existsSync(join(projectDir, '.openclaw', 'extensions', a)));
1913
2106
  if (folderExists) {
1914
2107
  sendLog(`[plugin] Warning: installation reported errors, but plugin folder successfully written. Proceeding.`);
1915
- return { stdout: '', stderr: err.message };
1916
2108
  } else {
1917
2109
  installSuccess = false;
1918
- throw err;
2110
+ // Re-enable in config on failure to restore state
2111
+ if (existsSync(cfgPath)) {
2112
+ try {
2113
+ const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
2114
+ cfg.plugins = cfg.plugins || { entries: {} };
2115
+ cfg.plugins.entries = cfg.plugins.entries || {};
2116
+ const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
2117
+ cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
2118
+ cfg.plugins.entries[existingKey].enabled = true;
2119
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2120
+ } catch (_) {}
2121
+ }
2122
+ throw new Error(cmdOut.stderr || cmdOut.stdout || `Failed to install plugin ${id} inside container.`);
1919
2123
  }
1920
- });
1921
- if (cmdOut) {
1922
- for (const line of `${cmdOut.stdout}\n${cmdOut.stderr}`.split(/\r?\n/).filter(Boolean)) sendLog(line);
1923
2124
  }
1924
2125
 
1925
- const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
1926
2126
  if (existsSync(cfgPath)) {
1927
2127
  const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
1928
2128
  cfg.plugins = cfg.plugins || { entries: {} };
1929
2129
  cfg.plugins.entries = cfg.plugins.entries || {};
1930
2130
  const pluginAliasMap = {
1931
- 'openclaw-zalo-mod': ['openclaw-zalo-mod', 'zalo-mod'],
1932
- 'openclaw-n8n-facebook-crawler': ['openclaw-n8n-facebook-crawler', 'openclaw-facebook-crawler', 'n8n-facebook-crawler'],
1933
- 'openclaw-facebook-poster': ['openclaw-facebook-poster', 'openclaw-n8n-facebook-poster', 'facebook-poster'],
2131
+ 'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
2132
+ 'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
2133
+ 'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
2134
+ 'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
1934
2135
  };
1935
2136
  const aliases = pluginAliasMap[id] || [id];
1936
2137
  const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
@@ -1946,12 +2147,8 @@ async function installFeature(projectDir, agentId, kind, id) {
1946
2147
  cfg.plugins.entries[existingKey].config.dashboardPort = gwPort + 1;
1947
2148
  }
1948
2149
  }
1949
- // Ensure plugins.allow includes this plugin so gateway loads it
1950
- cfg.plugins.allow = cfg.plugins.allow || [];
1951
- const allowIds = aliases.concat(existingKey);
1952
- for (const aid of allowIds) {
1953
- if (!cfg.plugins.allow.includes(aid)) cfg.plugins.allow.push(aid);
1954
- }
2150
+ // Only add the canonical config key to allow list (not all aliases)
2151
+ if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
1955
2152
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
1956
2153
  }
1957
2154
 
@@ -1977,14 +2174,23 @@ async function installFeature(projectDir, agentId, kind, id) {
1977
2174
  }
1978
2175
  }
1979
2176
 
1980
- sendLog(`[plugin] Restarting docker container to apply plugin...`);
1981
- if (isZaloMod && composeDir) {
2177
+ // Browser-automation plugin needs Docker rebuild for Playwright/Chromium deps
2178
+ const isBrowserPlugin = id === 'openclaw-browser-automation' || id === 'browser-automation';
2179
+ if (isBrowserPlugin && composeDir) {
2180
+ sendLog(`[plugin] Browser plugin requires Docker rebuild for Playwright/Chromium...`);
2181
+ const svcName = getBotServiceName(projectDir);
2182
+ await run('docker', ['compose', '-f', join(composeDir, 'docker-compose.yml'), 'up', '-d', '--build', '--force-recreate', svcName], { shell: false }).catch((err) => {
2183
+ sendLog(`[plugin] Docker rebuild failed: ${err.message}. Falling back to restart...`);
2184
+ return run('docker', ['restart', botContainer], { shell: false });
2185
+ });
2186
+ } else if (isZaloMod && composeDir) {
1982
2187
  // Use docker compose up to apply new port mappings from docker-compose.yml
1983
2188
  const svcName = getBotServiceName(projectDir);
1984
2189
  await run('docker', ['compose', '-f', join(composeDir, 'docker-compose.yml'), 'up', '-d', '--force-recreate', '--no-deps', svcName], { shell: false }).catch(() =>
1985
2190
  run('docker', ['restart', botContainer], { shell: false })
1986
2191
  );
1987
2192
  } else {
2193
+ sendLog(`[plugin] Restarting docker container to apply plugin...`);
1988
2194
  await run('docker', ['restart', botContainer], { shell: false });
1989
2195
  }
1990
2196
  } else {
@@ -2016,9 +2222,9 @@ async function installFeature(projectDir, agentId, kind, id) {
2016
2222
  cfg.plugins = cfg.plugins || { entries: {} };
2017
2223
  cfg.plugins.entries = cfg.plugins.entries || {};
2018
2224
  const pluginAliasMap = {
2019
- 'openclaw-zalo-mod': ['openclaw-zalo-mod', 'zalo-mod'],
2020
- 'openclaw-n8n-facebook-crawler': ['openclaw-n8n-facebook-crawler', 'openclaw-facebook-crawler', 'n8n-facebook-crawler'],
2021
- 'openclaw-facebook-poster': ['openclaw-facebook-poster', 'openclaw-n8n-facebook-poster', 'facebook-poster'],
2225
+ 'openclaw-zalo-mod': ['zalo-mod', 'openclaw-zalo-mod'],
2226
+ 'openclaw-facebook-crawler': ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
2227
+ 'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
2022
2228
  };
2023
2229
  const aliases = pluginAliasMap[id] || [id];
2024
2230
  const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
@@ -2041,15 +2247,36 @@ async function installFeature(projectDir, agentId, kind, id) {
2041
2247
  return { ok: true };
2042
2248
  }
2043
2249
 
2250
+ async function getInstalledPluginVersion(projectDir, aliases = []) {
2251
+ if (!projectDir) return '';
2252
+ try {
2253
+ const instPath = join(projectDir, '.openclaw', 'plugins', 'installs.json');
2254
+ if (existsSync(instPath)) {
2255
+ const j = JSON.parse(await fsp.readFile(instPath, 'utf8'));
2256
+ const found = (j.plugins || []).find(p => aliases.some(a => String(p.pluginId || '').toLowerCase() === String(a).toLowerCase()));
2257
+ if (found && found.version) return found.version;
2258
+ }
2259
+ } catch (e) {}
2260
+
2261
+ for (const alias of aliases) {
2262
+ try {
2263
+ const pkgPath = join(projectDir, '.openclaw', 'extensions', alias, 'package.json');
2264
+ if (existsSync(pkgPath)) {
2265
+ const pkg = JSON.parse(await fsp.readFile(pkgPath, 'utf8'));
2266
+ if (pkg.version) return pkg.version;
2267
+ }
2268
+ } catch (e) {}
2269
+ }
2270
+ return '';
2271
+ }
2272
+
2044
2273
  async function getFeatureFlags(projectDir, agentId = '') {
2045
2274
  const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
2046
2275
  const cfg = existsSync(cfgPath) ? ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'))) : {};
2047
2276
  const aid = agentId || cfg.agents?.list?.[0]?.id || 'bot';
2048
2277
  const browserOn = !!cfg.browser?.enabled;
2049
2278
  const cronOn = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
2050
- if (!browserOn) await applyFeatureToggle(projectDir, aid, 'skill', 'browser', true).catch(() => {});
2051
- if (!cronOn) await applyFeatureToggle(projectDir, aid, 'skill', 'cron', true).catch(() => {});
2052
- const fresh = existsSync(cfgPath) ? ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8').catch(() => '{}'))) : {};
2279
+ const fresh = cfg;
2053
2280
  const freshSaved = {};
2054
2281
  const installsPath = join(projectDir || '', '.openclaw', 'plugins', 'installs.json');
2055
2282
  const installs = existsSync(installsPath) ? JSON.parse(await fsp.readFile(installsPath, 'utf8').catch(() => '{}')) : {};
@@ -2076,16 +2303,18 @@ async function getFeatureFlags(projectDir, agentId = '') {
2076
2303
  allowSet.has(a)
2077
2304
  );
2078
2305
  const aliases = {
2306
+ browser: ['openclaw-browser-automation', 'browser-automation'],
2079
2307
  zalo: ['openclaw-zalo-mod', 'zalo-mod'],
2080
- crawler: ['openclaw-n8n-facebook-crawler', 'openclaw-facebook-crawler', 'n8n-facebook-crawler'],
2081
- poster: ['openclaw-facebook-poster', 'openclaw-n8n-facebook-poster', 'facebook-poster'],
2308
+ crawler: ['openclaw-facebook-crawler', 'openclaw-n8n-facebook-crawler', 'n8n-facebook-crawler'],
2309
+ poster: ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
2082
2310
  };
2083
2311
  const flags = {
2084
- 'skill:browser': freshSaved['skill:browser'] ?? true,
2085
- 'skill:cron': freshSaved['skill:cron'] ?? true,
2312
+ 'skill:browser': browserOn,
2313
+ 'skill:cron': cronOn,
2314
+ 'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
2086
2315
  'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
2087
- 'plugin:openclaw-n8n-facebook-crawler': isEnabled(aliases.crawler),
2088
- 'plugin:openclaw-facebook-poster': isEnabled(aliases.poster),
2316
+ 'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
2317
+ 'plugin:openclaw-n8n-facebook-poster': isEnabled(aliases.poster),
2089
2318
  };
2090
2319
  const extensionsDir = join(projectDir || '', '.openclaw', 'extensions');
2091
2320
  const extensionDirExists = (aliases = []) =>
@@ -2093,11 +2322,18 @@ async function getFeatureFlags(projectDir, agentId = '') {
2093
2322
  const isActuallyInstalled = (aliases = []) =>
2094
2323
  extensionDirExists(aliases) || isInstalledByRecord(aliases);
2095
2324
  const installed = {
2325
+ 'plugin:openclaw-browser-automation': isActuallyInstalled(aliases.browser),
2096
2326
  'plugin:openclaw-zalo-mod': isActuallyInstalled(aliases.zalo),
2097
- 'plugin:openclaw-n8n-facebook-crawler': isActuallyInstalled(aliases.crawler),
2098
- 'plugin:openclaw-facebook-poster': isActuallyInstalled(aliases.poster),
2327
+ 'plugin:openclaw-facebook-crawler': isActuallyInstalled(aliases.crawler),
2328
+ 'plugin:openclaw-n8n-facebook-poster': isActuallyInstalled(aliases.poster),
2329
+ };
2330
+ const versions = {
2331
+ 'plugin:openclaw-browser-automation': await getInstalledPluginVersion(projectDir, aliases.browser),
2332
+ 'plugin:openclaw-zalo-mod': await getInstalledPluginVersion(projectDir, aliases.zalo),
2333
+ 'plugin:openclaw-facebook-crawler': await getInstalledPluginVersion(projectDir, aliases.crawler),
2334
+ 'plugin:openclaw-n8n-facebook-poster': await getInstalledPluginVersion(projectDir, aliases.poster),
2099
2335
  };
2100
- return { flags, installed };
2336
+ return { flags, installed, versions };
2101
2337
  }
2102
2338
 
2103
2339
  async function serveStatic(req, res) {
@@ -2129,7 +2365,12 @@ async function handler(req, res, rootProjectDir) {
2129
2365
  }
2130
2366
  if (url.pathname === '/api/system' && req.method === 'GET') {
2131
2367
  const osChoice = detectOs();
2132
- const [nodeStatus, npmStatus, dockerStatus, currentVersions] = await Promise.all([commandExists('node'), commandExists('npm'), commandExists('docker', ['version', '--format', '{{.Server.Version}}']), getCurrentRuntimeVersions()]);
2368
+ const [nodeStatus, npmStatus, dockerStatus, currentVersions] = await Promise.all([
2369
+ commandExists('node'),
2370
+ commandExists('npm'),
2371
+ commandExists('docker', ['version', '--format', '{{.Server.Version}}']),
2372
+ getCurrentRuntimeVersions()
2373
+ ]);
2133
2374
  const projectDir = state.projectDir && existsSync(join(state.projectDir, '.openclaw', 'openclaw.json')) ? state.projectDir : null;
2134
2375
  const projectVersions = await resolveProjectRuntimeVersions(projectDir, state.mode).catch(() => null);
2135
2376
  const mergedVersions = {
@@ -2138,7 +2379,38 @@ async function handler(req, res, rootProjectDir) {
2138
2379
  node: projectVersions?.node || currentVersions.node || String(nodeStatus?.output || '').trim(),
2139
2380
  };
2140
2381
  const projects = await discoverProjects(rootProjectDir).catch(() => []);
2141
- return json(res, { os: osChoice, platform: process.platform, arch: process.arch, recommendedMode: recommendedMode(osChoice), node: nodeStatus, npm: npmStatus, docker: dockerStatus, versions: { desiredOpenclaw: OPENCLAW_NPM_SPEC, desiredNineRouter: NINE_ROUTER_NPM_SPEC, currentOpenclaw: mergedVersions.openclaw, currentNineRouter: mergedVersions.nineRouter, currentNode: mergedVersions.node, openclaw: mergedVersions.openclaw, nineRouter: mergedVersions.nineRouter, node: mergedVersions.node }, projects });
2382
+
2383
+ let latestSetupVersion = SETUP_VERSION;
2384
+ try {
2385
+ const resp = await fetch('https://registry.npmjs.org/create-openclaw-bot/latest', { signal: AbortSignal.timeout(3000) });
2386
+ if (resp.ok) {
2387
+ const data = await resp.json();
2388
+ if (data.version) latestSetupVersion = data.version;
2389
+ }
2390
+ } catch (e) {}
2391
+
2392
+ return json(res, {
2393
+ os: osChoice,
2394
+ platform: process.platform,
2395
+ arch: process.arch,
2396
+ recommendedMode: recommendedMode(osChoice),
2397
+ node: nodeStatus,
2398
+ npm: npmStatus,
2399
+ docker: dockerStatus,
2400
+ versions: {
2401
+ desiredOpenclaw: OPENCLAW_NPM_SPEC,
2402
+ desiredNineRouter: NINE_ROUTER_NPM_SPEC,
2403
+ currentOpenclaw: mergedVersions.openclaw,
2404
+ currentNineRouter: mergedVersions.nineRouter,
2405
+ currentNode: mergedVersions.node,
2406
+ openclaw: mergedVersions.openclaw,
2407
+ nineRouter: mergedVersions.nineRouter,
2408
+ node: mergedVersions.node,
2409
+ setup: SETUP_VERSION,
2410
+ latestSetup: latestSetupVersion
2411
+ },
2412
+ projects
2413
+ });
2142
2414
  }
2143
2415
  if (url.pathname === '/api/projects/discover' && req.method === 'GET') {
2144
2416
  return json(res, { ok: true, projects: await discoverProjects(rootProjectDir).catch(() => []) });
@@ -2219,6 +2491,35 @@ async function handler(req, res, rootProjectDir) {
2219
2491
  sendLog(`[update] ${target} update completed (${result.mode})`);
2220
2492
  return json(res, result);
2221
2493
  }
2494
+ if (url.pathname === '/api/setup/update' && req.method === 'POST') {
2495
+ sendLog('[update-setup] Starting update of Setup Wizard...');
2496
+ const isGit = existsSync(resolve(rootProjectDir, '.git'));
2497
+ if (isGit) {
2498
+ sendLog('[update-setup] Git repository detected. Pulling latest code and building...');
2499
+ setImmediate(async () => {
2500
+ try {
2501
+ await run('git', ['pull'], { cwd: rootProjectDir });
2502
+ await run('npm', ['install'], { cwd: rootProjectDir });
2503
+ await run('npm', ['run', 'build'], { cwd: rootProjectDir });
2504
+ sendLog('[update-setup] Setup Wizard updated successfully! Please restart the installer.');
2505
+ } catch (err) {
2506
+ sendLog(`[update-setup] Error updating: ${err.message}`);
2507
+ }
2508
+ });
2509
+ return json(res, { ok: true, mode: 'git' });
2510
+ } else {
2511
+ sendLog('[update-setup] Global npm package installation detected. Updating via npm...');
2512
+ setImmediate(async () => {
2513
+ try {
2514
+ await run('npm', ['install', '-g', 'create-openclaw-bot@latest'], { cwd: rootProjectDir });
2515
+ sendLog('[update-setup] Setup Wizard updated successfully! Please restart the installer.');
2516
+ } catch (err) {
2517
+ sendLog(`[update-setup] Error updating: ${err.message}`);
2518
+ }
2519
+ });
2520
+ return json(res, { ok: true, mode: 'npm' });
2521
+ }
2522
+ }
2222
2523
  if (url.pathname === '/api/bot/create' && req.method === 'POST') {
2223
2524
  const body = await readJson(req);
2224
2525
  const projectDir = await resolveProjectDir(rootProjectDir, body);
@@ -2311,9 +2612,10 @@ async function handler(req, res, rootProjectDir) {
2311
2612
  { name: 'Cron', slug: 'cron' },
2312
2613
  ],
2313
2614
  plugins: [
2615
+ { name: 'openclaw-browser-automation', package: 'openclaw-browser-automation' },
2314
2616
  { name: 'openclaw-zalo-mod', package: 'openclaw-zalo-mod' },
2315
- { name: 'openclaw-n8n-facebook-crawler', package: 'openclaw-n8n-facebook-crawler' },
2316
- { name: 'openclaw-facebook-poster', package: 'openclaw-facebook-poster' },
2617
+ { name: 'openclaw-facebook-crawler', package: 'openclaw-facebook-crawler' },
2618
+ { name: 'openclaw-n8n-facebook-poster', package: 'openclaw-n8n-facebook-poster' },
2317
2619
  ]
2318
2620
  });
2319
2621
  if (url.pathname === '/api/features' && req.method === 'GET') {