create-openclaw-bot 5.8.3 → 5.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  # 🦞 OpenClaw Setup
4
4
 
5
5
  <p align="center">
6
- <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.8.3-0EA5E9?style=for-the-badge" alt="Version 5.8.3" /></a>
6
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.8.5-0EA5E9?style=for-the-badge" alt="Version 5.8.5" /></a>
7
7
  <a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
8
8
  <a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
9
9
  <a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
@@ -23,7 +23,7 @@ A next-generation **Web UI Setup** and management dashboard that automates 100%
23
23
 
24
24
  ---
25
25
 
26
- ## 🆕 What's New in v5.8.3
26
+ ## 🆕 What's New in v5.8.5
27
27
 
28
28
  - 🔄 **Smart Header Update Button**: Instantly upgrades the setup wizard from the UI! The button queries the npm registry dynamically and only reveals itself when a newer release is published.
29
29
  - 📡 **Live Log-Streaming Upgrade**: Kicking off the update automatically executes the migration (running `git pull && npm install && npm run build` for Git clones, or `npm install -g create-openclaw-bot` for NPM installs) while streaming standard outputs in real-time straight to the dashboard's Logs terminal.
package/README.vi.md CHANGED
@@ -3,7 +3,7 @@
3
3
  # 🦞 OpenClaw Setup
4
4
 
5
5
  <p align="center">
6
- <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.8.3-0EA5E9?style=for-the-badge" alt="Version 5.8.3" /></a>
6
+ <a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.8.5-0EA5E9?style=for-the-badge" alt="Version 5.8.5" /></a>
7
7
  <a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
8
8
  <a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
9
9
  <a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
@@ -23,7 +23,7 @@ Trình cài đặt và quản trị **Web UI Setup** thế hệ mới giúp tự
23
23
 
24
24
  ---
25
25
 
26
- ## 🆕 Có gì mới trong v5.8.3
26
+ ## 🆕 Có gì mới trong v5.8.5
27
27
 
28
28
  - 🔄 **Nút cập nhật Header thông minh**: Nâng cấp trực tiếp setup wizard từ giao diện! Nút cập nhật tự truy vấn npm registry và chỉ hiển thị khi có phiên bản mới hơn.
29
29
  - 📡 **Nâng cấp Stream Log trực tiếp**: Khởi chạy cập nhật sẽ tự động nâng cấp (chạy `git pull && npm install && npm run build` cho bản Git, hoặc `npm install -g create-openclaw-bot` cho bản NPM) và đẩy dòng log theo thời gian thực về tab Logs.
package/dist/cli.js CHANGED
@@ -32,21 +32,17 @@ const args = process.argv.slice(2);
32
32
  if (isLocalRepo()) {
33
33
  const { startLocalInstaller } = await import('./server/local-server.js');
34
34
 
35
- if (args[0] === 'legacy' || args.includes('--legacy')) {
36
- await import('./legacy-cli.js');
37
- } else {
38
- const noOpen = args.includes('--no-open');
39
- const hostArg = args.find((arg) => arg.startsWith('--host='));
40
- const portArg = args.find((arg) => arg.startsWith('--port='));
41
- const projectDirArg = args.find((arg) => arg.startsWith('--project-dir='));
42
-
43
- await startLocalInstaller({
44
- openBrowser: !noOpen,
45
- host: hostArg ? hostArg.slice('--host='.length) : '127.0.0.1',
46
- preferredPort: portArg ? Number(portArg.slice('--port='.length)) : 51789,
47
- projectDir: projectDirArg ? projectDirArg.slice('--project-dir='.length) : process.cwd(),
48
- });
49
- }
35
+ const noOpen = args.includes('--no-open');
36
+ const hostArg = args.find((arg) => arg.startsWith('--host='));
37
+ const portArg = args.find((arg) => arg.startsWith('--port='));
38
+ const projectDirArg = args.find((arg) => arg.startsWith('--project-dir='));
39
+
40
+ await startLocalInstaller({
41
+ openBrowser: !noOpen,
42
+ host: hostArg ? hostArg.slice('--host='.length) : '127.0.0.1',
43
+ preferredPort: portArg ? Number(portArg.slice('--port='.length)) : 51789,
44
+ projectDir: projectDirArg ? projectDirArg.slice('--project-dir='.length) : process.cwd(),
45
+ });
50
46
  } else {
51
47
  console.log('\n============================================================');
52
48
  console.log(' 🦞 OpenClaw Setup — Auto-downloader & Installer');
@@ -13,7 +13,7 @@ function loadSharedModule(modulePath, globalName) {
13
13
  if (loaded && Object.keys(loaded).length > 0) return loaded;
14
14
  return globalThis[globalName] || loaded || {};
15
15
  }
16
- const { buildWorkspaceFileMap } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
16
+ const { buildWorkspaceFileMap, buildCronjobSkillMd, buildInfographicGeneratorSkillMd, buildInfographicGeneratorJs } = loadSharedModule('../setup/shared/workspace-gen.js', '__openclawWorkspace');
17
17
  const { buildOpenclawJson, buildEnvFileContent, buildExecApprovalsJson } = loadSharedModule('../setup/shared/bot-config-gen.js', '__openclawBotConfig');
18
18
  const { buildDockerArtifacts } = loadSharedModule('../setup/shared/docker-gen.js', '__openclawDockerGen');
19
19
  const { OPENCLAW_NPM_SPEC, NINE_ROUTER_NPM_SPEC, build9RouterProviderConfig, get9RouterBaseUrl } = loadSharedModule('../setup/shared/common-gen.js', '__openclawCommon');
@@ -22,11 +22,33 @@ const dataExport = loadSharedModule('../setup/data/index.js', '__openclawData');
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
23
  const WEB_DIR = resolve(__dirname, '../web');
24
24
  const SETUP_VERSION = (() => { try { return JSON.parse(fs.readFileSync(resolve(__dirname, '../../package.json'), 'utf8')).version || '0.0.0'; } catch { return '0.0.0'; } })();
25
+ let latestSetupVersionCache = SETUP_VERSION;
26
+ let isFetchingLatestSetup = false;
27
+
28
+ async function fetchLatestSetupVersionBg() {
29
+ if (isFetchingLatestSetup) return;
30
+ isFetchingLatestSetup = true;
31
+ try {
32
+ const resp = await fetch('https://registry.npmjs.org/create-openclaw-bot/latest', { signal: AbortSignal.timeout(4000) });
33
+ if (resp.ok) {
34
+ const data = await resp.json();
35
+ if (data.version) {
36
+ latestSetupVersionCache = data.version;
37
+ }
38
+ }
39
+ } catch (e) {
40
+ } finally {
41
+ isFetchingLatestSetup = false;
42
+ }
43
+ }
44
+ fetchLatestSetupVersionBg().catch(() => {});
45
+
25
46
  const DEFAULT_PROJECT_NAME = 'openclaw-bot';
26
47
  const STATE_FILE = '.openclaw-setup-state.json';
27
48
  const DEFAULT_MODEL = 'smart-route';
28
49
  const logClients = new Set();
29
50
  let zaloLoginInFlight = false;
51
+ let activeServerInstance = null;
30
52
  const state = {
31
53
  installing: false,
32
54
  installed: false,
@@ -901,7 +923,32 @@ async function buildBotStatus() {
901
923
  resolveProjectRuntimeVersions(state.projectDir, state.mode).catch(() => ({ openclaw: '', nineRouter: '', node: process.version || '' })),
902
924
  ]);
903
925
  const credentials = await readBotCredentials(state.projectDir).catch(() => ({ openclawToken: '', nineRouterApiKey: '' }));
904
- return { ...state, gatewayStatus, routerStatus, bots, credentials, runtimeVersions };
926
+
927
+ let activeModel = 'smart-route';
928
+ let activeProvider = '9Router';
929
+ if (state.projectDir) {
930
+ const cfgPath = join(state.projectDir, '.openclaw', 'openclaw.json');
931
+ if (existsSync(cfgPath)) {
932
+ try {
933
+ const raw = await fsp.readFile(cfgPath, 'utf8');
934
+ const cfg = JSON.parse(raw);
935
+ const modelStr = cfg.agents?.defaults?.model?.primary || cfg.agents?.list?.[0]?.model?.primary || 'smart-route';
936
+ if (modelStr.includes('/')) {
937
+ const parts = modelStr.split('/');
938
+ activeProvider = parts[0];
939
+ activeModel = parts.slice(1).join('/');
940
+ } else {
941
+ activeModel = modelStr;
942
+ activeProvider = cfg.models?.providers?.openai ? 'openai' : '9router';
943
+ }
944
+ } catch (e) {}
945
+ }
946
+ }
947
+
948
+ const cap = (s) => String(s).toLowerCase() === 'openai' ? 'OpenAI' : String(s).toLowerCase() === '9router' ? '9Router' : s;
949
+ activeProvider = cap(activeProvider);
950
+
951
+ return { ...state, gatewayStatus, routerStatus, bots, credentials, runtimeVersions, activeModel, activeProvider };
905
952
  }
906
953
 
907
954
  async function createBotInProject(projectDir, body = {}, runtime = {}) {
@@ -933,10 +980,16 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
933
980
  agentMetas: [],
934
981
  }));
935
982
 
936
- const existingAgentCount = cfg.agents.list.length;
937
983
  const used = new Set(cfg.agents.list.map((a) => a.id));
938
984
  const botName = uniqueDisplayName(requestedBotName, new Set(cfg.agents.list.map((a) => a.name || a.id)));
939
- const agentId = uniqueSlug(slugify(botName), used);
985
+ let agentId = body.agentId ? String(body.agentId).trim().toLowerCase().replace(/[^a-z0-9-_]+/g, '-') : '';
986
+ if (!agentId) {
987
+ agentId = uniqueSlug(slugify(botName), used);
988
+ } else {
989
+ if (used.has(agentId)) {
990
+ throw httpError(400, `Bot ID "${agentId}" đã tồn tại. Vui lòng chọn ID khác.`);
991
+ }
992
+ }
940
993
  const workspaceDir = `workspace-${agentId}`;
941
994
  const model = cfg.agents.defaults?.model?.primary || cfg.agents.list[0]?.model?.primary || DEFAULT_MODEL;
942
995
  cfg.agents.list.push({
@@ -980,6 +1033,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
980
1033
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
981
1034
 
982
1035
  const hasScheduler = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
1036
+ const hasImageGen = !!(cfg.skills?.entries?.['image-gen']?.enabled);
983
1037
  const files = buildWorkspaceFileMap({
984
1038
  isVi: true,
985
1039
  botName,
@@ -991,6 +1045,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
991
1045
  workspacePath: `.openclaw/${workspaceDir}`,
992
1046
  hasZaloMod: channel === 'zalo-personal',
993
1047
  hasScheduler,
1048
+ hasImageGen,
994
1049
  });
995
1050
  const wsRoot = join(openclawHome, workspaceDir);
996
1051
  for (const [name, content] of Object.entries(files)) {
@@ -1080,6 +1135,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1080
1135
  }
1081
1136
 
1082
1137
  const hasScheduler = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
1138
+ const hasImageGen = !!(cfg.skills?.entries?.['image-gen']?.enabled);
1083
1139
  const files = buildWorkspaceFileMap({
1084
1140
  isVi: true,
1085
1141
  botName,
@@ -1091,6 +1147,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1091
1147
  workspacePath: `.openclaw/${workspaceDir}`,
1092
1148
  hasZaloMod: channel === 'zalo-personal',
1093
1149
  hasScheduler,
1150
+ hasImageGen,
1094
1151
  });
1095
1152
  const wsRoot = join(projectDir, '.openclaw', workspaceDir);
1096
1153
  for (const [name, content] of Object.entries(files)) {
@@ -1531,10 +1588,9 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
1531
1588
  await fsp.mkdir(openclawHome, { recursive: true });
1532
1589
  await fsp.mkdir(join(openclawHome, 'plugin-runtime-deps'), { recursive: true });
1533
1590
 
1534
- const selectedSkills = [];
1535
- const botName = 'OpenClaw Bot';
1536
- const agentMetas = [{ agentId: 'bot', name: botName, description: 'Personal OpenClaw assistant' }];
1537
- const common = { botName, channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
1591
+ const selectedSkills = ['memory', 'image-gen', 'web-search'];
1592
+ const agentMetas = [];
1593
+ const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
1538
1594
  const cfg = buildOpenclawJson(common);
1539
1595
  const env = buildEnvFileContent({ ...common, apiKey: '', botToken: '' });
1540
1596
  const approvals = buildExecApprovalsJson({ agentMetas });
@@ -1543,26 +1599,6 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
1543
1599
  await fsp.writeFile(join(projectDir, '.env'), env, 'utf8');
1544
1600
  await fsp.writeFile(join(openclawHome, 'exec-approvals.json'), JSON.stringify(approvals, null, 2), 'utf8');
1545
1601
 
1546
- const workspaceDir = 'workspace-bot';
1547
- const workspace = buildWorkspaceFileMap({
1548
- isVi: true,
1549
- botName,
1550
- channelKey: 'telegram',
1551
- providerKey: '9router',
1552
- selectedSkills,
1553
- skillsCatalog: dataExport.SKILLS || [],
1554
- agentMetas,
1555
- deployMode: mode,
1556
- osChoice,
1557
- agentWorkspaceDir: workspaceDir,
1558
- workspacePath: `.openclaw/${workspaceDir}`,
1559
- });
1560
- const wsRoot = join(openclawHome, workspaceDir);
1561
- for (const [name, content] of Object.entries(workspace)) {
1562
- await fsp.mkdir(dirname(join(wsRoot, name)), { recursive: true });
1563
- await fsp.writeFile(join(wsRoot, name), content || '', 'utf8');
1564
- }
1565
-
1566
1602
  if (mode === 'docker') {
1567
1603
  const projectName = slugify(basename(projectDir)) || 'bot';
1568
1604
  const docker = buildDockerArtifacts({
@@ -1616,7 +1652,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
1616
1652
  await fsp.mkdir(dockerDir, { recursive: true });
1617
1653
  const envContent = existsSync(rootEnvPath)
1618
1654
  ? await fsp.readFile(rootEnvPath, 'utf8')
1619
- : buildEnvFileContent({ botName: 'OpenClaw Bot', channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: [], skills: dataExport.SKILLS || [], agentMetas: [{ agentId: 'bot', name: 'OpenClaw Bot', description: 'Personal OpenClaw assistant' }], apiKey: '', botToken: '' });
1655
+ : buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: [], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
1620
1656
  await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
1621
1657
  sendLog(`Docker env ready: ${dockerEnvPath}`);
1622
1658
  await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
@@ -1735,6 +1771,29 @@ async function loadSavedState(rootProjectDir) {
1735
1771
  }
1736
1772
  }
1737
1773
 
1774
+ function isRestrictedSystemDir(dirPath) {
1775
+ if (!dirPath) return true;
1776
+ const lower = resolve(dirPath).toLowerCase();
1777
+
1778
+ if (SYSTEM_DIR_BLACKLIST.has(basename(lower))) return true;
1779
+
1780
+ const winDir = process.env.SystemRoot ? resolve(process.env.SystemRoot).toLowerCase() : 'c:\\windows';
1781
+ const programFiles = process.env.ProgramFiles ? resolve(process.env.ProgramFiles).toLowerCase() : 'c:\\program files';
1782
+ const programFilesX86 = process.env['ProgramFiles(x86)'] ? resolve(process.env['ProgramFiles(x86)']).toLowerCase() : 'c:\\program files (x86)';
1783
+
1784
+ if (lower.startsWith(winDir) || lower.startsWith(programFiles) || lower.startsWith(programFilesX86)) {
1785
+ return true;
1786
+ }
1787
+
1788
+ if (lower.includes(':\\users\\') || lower.endsWith(':\\users')) {
1789
+ const home = resolve(os.homedir()).toLowerCase();
1790
+ if (lower !== home && !lower.startsWith(home + '\\') && !lower.startsWith(home + '/')) {
1791
+ return true;
1792
+ }
1793
+ }
1794
+ return false;
1795
+ }
1796
+
1738
1797
  async function findLatestProject(rootProjectDir) {
1739
1798
  const roots = [
1740
1799
  process.env.OPENCLAW_PROJECT_DIR,
@@ -1743,29 +1802,39 @@ async function findLatestProject(rootProjectDir) {
1743
1802
  join(rootProjectDir, DEFAULT_PROJECT_NAME),
1744
1803
  dirname(rootProjectDir),
1745
1804
  os.homedir(),
1746
- ];
1747
- // Scan all available drives, walking top-level dirs but skipping system folders
1805
+ join(os.homedir(), 'Documents'),
1806
+ ].filter(Boolean);
1807
+
1748
1808
  const drives = await getAvailableDrives();
1749
1809
  for (const drive of drives) {
1750
1810
  const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
1751
1811
  for (const e of entries) {
1752
1812
  if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
1753
- roots.push(join(drive, e.name));
1813
+ const fullPath = join(drive, e.name);
1814
+ if (!isRestrictedSystemDir(fullPath)) {
1815
+ roots.push(fullPath);
1816
+ }
1754
1817
  }
1755
1818
  }
1756
1819
  }
1757
1820
  const candidates = [];
1821
+ const seen = new Set();
1758
1822
  async function walk(dir, depth = 0) {
1759
1823
  if (!dir || depth > 2 || !existsSync(dir)) return;
1760
- if (existsSync(join(dir, '.openclaw', 'openclaw.json'))) {
1761
- const st = await fsp.stat(join(dir, '.openclaw', 'openclaw.json')).catch(() => null);
1762
- if (st) candidates.push({ dir, mtimeMs: st.mtimeMs });
1824
+ const full = resolve(dir);
1825
+ if (isRestrictedSystemDir(full)) return;
1826
+ if (seen.has(full)) return;
1827
+ seen.add(full);
1828
+
1829
+ if (existsSync(join(full, '.openclaw', 'openclaw.json'))) {
1830
+ const st = await fsp.stat(join(full, '.openclaw', 'openclaw.json')).catch(() => null);
1831
+ if (st) candidates.push({ dir: full, mtimeMs: st.mtimeMs });
1763
1832
  return;
1764
1833
  }
1765
- const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
1834
+ const entries = await fsp.readdir(full, { withFileTypes: true }).catch(() => []);
1766
1835
  for (const e of entries) {
1767
1836
  if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
1768
- await walk(join(dir, e.name), depth + 1);
1837
+ await walk(join(full, e.name), depth + 1);
1769
1838
  }
1770
1839
  }
1771
1840
  }
@@ -1780,16 +1849,28 @@ async function discoverProjects(rootProjectDir) {
1780
1849
  rootProjectDir,
1781
1850
  dirname(rootProjectDir),
1782
1851
  process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
1783
- ];
1784
- // Add all available drives for scanning
1852
+ os.homedir(),
1853
+ join(os.homedir(), 'Documents'),
1854
+ ].filter(Boolean);
1855
+
1785
1856
  const drives = await getAvailableDrives();
1786
- for (const drive of drives) roots.push(drive);
1857
+ for (const drive of drives) {
1858
+ const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
1859
+ for (const e of entries) {
1860
+ if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
1861
+ const fullPath = join(drive, e.name);
1862
+ if (!isRestrictedSystemDir(fullPath)) {
1863
+ roots.push(fullPath);
1864
+ }
1865
+ }
1866
+ }
1867
+ }
1787
1868
  const seen = new Set();
1788
1869
  const hits = [];
1789
1870
  async function walk(dir, depth = 0) {
1790
1871
  if (!dir || depth > 2 || !existsSync(dir)) return;
1791
1872
  const full = resolve(dir);
1792
- if (full === resolve(os.homedir())) return;
1873
+ if (isRestrictedSystemDir(full)) return;
1793
1874
  if (seen.has(full)) return;
1794
1875
  seen.add(full);
1795
1876
  const cfgPath = join(full, '.openclaw', 'openclaw.json');
@@ -1817,7 +1898,7 @@ async function discoverProjects(rootProjectDir) {
1817
1898
  const entries = await fsp.readdir(full, { withFileTypes: true }).catch(() => []);
1818
1899
  for (const e of entries) {
1819
1900
  if (!e.isDirectory()) continue;
1820
- if (e.name === 'node_modules' || e.name.startsWith('.git') || SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) continue;
1901
+ if (e.name === 'node_modules' || e.name.startsWith('.') || SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) continue;
1821
1902
  await walk(join(full, e.name), depth + 1);
1822
1903
  }
1823
1904
  }
@@ -1971,72 +2052,23 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
1971
2052
  const k = `${kind}:${id}`;
1972
2053
 
1973
2054
  if (kind === 'skill' && id === 'browser') {
2055
+ delete cfg.browser;
2056
+ cfg.plugins = cfg.plugins || { entries: {} };
2057
+ cfg.plugins.entries = cfg.plugins.entries || {};
2058
+ const aliases = ['browser-automation', 'openclaw-browser-automation'];
2059
+ const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
2060
+ cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
2061
+ cfg.plugins.entries[existingKey].enabled = !!enabled;
2062
+ cfg.plugins.allow = cfg.plugins.allow || [];
1974
2063
  if (enabled) {
1975
- cfg.browser = {
1976
- enabled: true,
1977
- defaultProfile: 'host-chrome',
1978
- profiles: { 'host-chrome': { cdpUrl: 'http://127.0.0.1:9222', color: '#4285F4' } },
1979
- };
1980
- const isHeadlessServer = process.platform === 'linux';
1981
- const docVariant = 'cli-server';
1982
-
1983
- for (const a of cfg.agents.list) {
1984
- const wm = buildWorkspaceFileMap({
1985
- isVi: true,
1986
- botName: a.name || a.id,
1987
- botDesc: '',
1988
- hasBrowser: false,
1989
- hasScheduler: true,
1990
- workspacePath: `.openclaw/${workspaceRelForAgent(a, cfg, projectDir)}/`,
1991
- agentWorkspaceDir: workspaceRelForAgent(a, cfg, projectDir),
1992
- variant: cfg.agents.list.length > 1 ? 'relay' : 'single',
1993
- browserDocVariant: docVariant,
1994
- });
1995
- const browserDoc = wm['BROWSER.md'] || '# BROWSER';
1996
- const browserTool = wm['browser-tool.js'] || '';
1997
- const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
1998
- await fsp.writeFile(bf.file, browserDoc, 'utf8');
1999
- const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
2000
- if (browserTool) await fsp.writeFile(bt.file, browserTool, 'utf8');
2001
-
2002
- const af = await readWorkspaceText(projectDir, a, 'AGENTS.md');
2003
- const agentsManaged = upsertManagedBlock(af.content, 'BROWSER_LINK', '- Browser docs: `BROWSER.md`');
2004
- await fsp.writeFile(af.file, agentsManaged, 'utf8');
2005
-
2006
- // Add to TOOLS.md
2007
- const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
2008
- const browserGuide = isHeadlessServer
2009
- ? `## 🌐 Browser Automation
2010
- - Xem hướng dẫn chi tiết tại **BROWSER.md**
2011
- - Script điều khiển: \`browser-tool.js\`
2012
- - Chế độ hiện tại: Chạy ngầm độc lập qua Docker hoặc Xvfb trên VPS.
2013
- - **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!
2014
- - Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`
2015
- : `## 🌐 Browser Automation
2016
- - Xem hướng dẫn chi tiết tại **BROWSER.md**
2017
- - Script điều khiển: \`browser-tool.js\`
2018
- - Chế độ hiện tại:
2019
- - **Mặc định:** Chạy ngầm độc lập qua Docker hoặc Server.
2020
- - **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.
2021
- - Kết nối mặc định: \`http://127.0.0.1:9222\`
2022
- - **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!
2023
- - Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`;
2024
- await fsp.writeFile(tf.file, upsertManagedBlock(tf.content, 'BROWSER_GUIDE', browserGuide), 'utf8');
2025
- }
2064
+ if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
2026
2065
  } else {
2027
- delete cfg.browser;
2066
+ cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
2028
2067
  for (const a of cfg.agents.list) {
2029
2068
  const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
2030
2069
  if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
2031
2070
  const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
2032
2071
  if (existsSync(bt.file)) await fsp.rm(bt.file, { force: true });
2033
-
2034
- const af = await readWorkspaceText(projectDir, a, 'AGENTS.md');
2035
- await fsp.writeFile(af.file, removeManagedBlock(af.content, 'BROWSER_LINK'), 'utf8');
2036
-
2037
- // Remove from TOOLS.md
2038
- const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
2039
- await fsp.writeFile(tf.file, removeManagedBlock(tf.content, 'BROWSER_GUIDE'), 'utf8');
2040
2072
  }
2041
2073
  }
2042
2074
 
@@ -2059,37 +2091,17 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2059
2091
  cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:automation']));
2060
2092
  cfg.commands = cfg.commands || {};
2061
2093
  cfg.commands.ownerAllowFrom = Array.from(new Set([...(cfg.commands.ownerAllowFrom || []), '*']));
2062
- const cronGuide = `## ⏰ Cron / Lên lịch nhắc nhở (tool: \`cron\`)
2063
- - **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).
2064
- - **⛔ 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\`.
2065
- - **Khi tạo cronjob mới (action \`add\`):**
2066
- - **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 đó.
2067
- - 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.
2068
- - **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.
2069
- - **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ờ.
2070
- - **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\`.
2071
- - **Khi user yêu cầu tắt/bật/xóa cronjob:**
2072
- 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.
2073
- 2. **Bước 2 (Xử lý):**
2074
- - Để xóa: Gọi action \`remove\` với \`id\` tìm được.
2075
- - Để tắt/tạm dừng: Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
2076
- - Để bật lại: Gọi action \`update\` với \`id\` và patch \`{"enabled": true}\`.
2077
- 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ế.
2078
- - 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.
2079
- - Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.
2080
- - **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.
2081
- - **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\`.
2082
- - 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.`;
2083
2094
  for (const a of cfg.agents.list) {
2084
- const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
2085
- await fsp.writeFile(tf.file, upsertManagedBlock(tf.content, 'CRON_GUIDE', cronGuide), 'utf8');
2095
+ const sf = await readWorkspaceText(projectDir, a, 'skills/cronjob/SKILL.md');
2096
+ await fsp.mkdir(dirname(sf.file), { recursive: true });
2097
+ await fsp.writeFile(sf.file, buildCronjobSkillMd(true), 'utf8');
2086
2098
  }
2087
2099
  } else {
2088
2100
  if (cfg.tools?.alsoAllow) cfg.tools.alsoAllow = cfg.tools.alsoAllow.filter((x) => x !== 'group:automation');
2089
2101
  if (cfg.commands?.ownerAllowFrom) cfg.commands.ownerAllowFrom = cfg.commands.ownerAllowFrom.filter((x) => x !== '*');
2090
2102
  for (const a of cfg.agents.list) {
2091
- const tf = await readWorkspaceText(projectDir, a, 'TOOLS.md');
2092
- await fsp.writeFile(tf.file, removeManagedBlock(tf.content, 'CRON_GUIDE'), 'utf8');
2103
+ const sf = await readWorkspaceText(projectDir, a, 'skills/cronjob/SKILL.md');
2104
+ if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
2093
2105
  }
2094
2106
  }
2095
2107
 
@@ -2104,6 +2116,59 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2104
2116
  }
2105
2117
  }
2106
2118
 
2119
+ if (kind === 'skill' && id === 'image-gen') {
2120
+ cfg.skills = cfg.skills || { entries: {} };
2121
+ cfg.skills.entries = cfg.skills.entries || {};
2122
+ cfg.skills.entries['image-gen'] = cfg.skills.entries['image-gen'] || {};
2123
+ cfg.skills.entries['image-gen'].enabled = !!enabled;
2124
+
2125
+ for (const a of cfg.agents.list) {
2126
+ const sf = await readWorkspaceText(projectDir, a, 'skills/infographic-generator/SKILL.md');
2127
+ const js = await readWorkspaceText(projectDir, a, 'skills/infographic-generator/image-generator.js');
2128
+ if (enabled) {
2129
+ await fsp.mkdir(dirname(sf.file), { recursive: true });
2130
+ await fsp.writeFile(sf.file, buildInfographicGeneratorSkillMd(), 'utf8');
2131
+ await fsp.writeFile(js.file, buildInfographicGeneratorJs(), 'utf8');
2132
+ } else {
2133
+ if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
2134
+ if (existsSync(js.file)) await fsp.rm(js.file, { force: true });
2135
+ }
2136
+ }
2137
+
2138
+ // Write cfgPath early so recreation reads updated openclaw.json
2139
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2140
+
2141
+ // Recreate container to apply updated openclaw.json
2142
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2143
+ if (hasDocker) {
2144
+ sendLog(`[docker] Infographic skill toggled to ${enabled}. Recreating containers...`);
2145
+ await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
2146
+ }
2147
+ }
2148
+
2149
+ if (kind === 'skill' && id === 'web-search') {
2150
+ cfg.plugins = cfg.plugins || { entries: {} };
2151
+ cfg.plugins.entries = cfg.plugins.entries || {};
2152
+ cfg.plugins.entries['duckduckgo'] = cfg.plugins.entries['duckduckgo'] || {};
2153
+ cfg.plugins.entries['duckduckgo'].enabled = !!enabled;
2154
+ cfg.plugins.allow = cfg.plugins.allow || [];
2155
+ if (enabled) {
2156
+ if (!cfg.plugins.allow.includes('duckduckgo')) cfg.plugins.allow.push('duckduckgo');
2157
+ } else {
2158
+ cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== 'duckduckgo');
2159
+ }
2160
+
2161
+ // Write cfgPath early so recreation reads updated openclaw.json
2162
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2163
+
2164
+ // Recreate container to apply updated openclaw.json
2165
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2166
+ if (hasDocker) {
2167
+ sendLog(`[docker] Web Search skill toggled to ${enabled}. Recreating containers...`);
2168
+ await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
2169
+ }
2170
+ }
2171
+
2107
2172
  if (kind === 'plugin') {
2108
2173
  cfg.plugins = cfg.plugins || { entries: {} };
2109
2174
  cfg.plugins.entries = cfg.plugins.entries || {};
@@ -2344,6 +2409,8 @@ async function getFeatureFlags(projectDir, agentId = '') {
2344
2409
  Array.from(installedSpecs).some((spec) => spec.includes(a)) ||
2345
2410
  allowSet.has(a)
2346
2411
  );
2412
+ const imageGenOn = !!cfg.skills?.entries?.['image-gen']?.enabled;
2413
+ const webSearchOn = isEnabled(['duckduckgo']);
2347
2414
  const aliases = {
2348
2415
  browser: ['openclaw-browser-automation', 'browser-automation'],
2349
2416
  zalo: ['openclaw-zalo-mod', 'zalo-mod'],
@@ -2353,6 +2420,8 @@ async function getFeatureFlags(projectDir, agentId = '') {
2353
2420
  const flags = {
2354
2421
  'skill:browser': browserOn,
2355
2422
  'skill:cron': cronOn,
2423
+ 'skill:image-gen': imageGenOn,
2424
+ 'skill:web-search': webSearchOn,
2356
2425
  'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
2357
2426
  'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
2358
2427
  'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
@@ -2422,14 +2491,8 @@ async function handler(req, res, rootProjectDir) {
2422
2491
  };
2423
2492
  const projects = await discoverProjects(rootProjectDir).catch(() => []);
2424
2493
 
2425
- let latestSetupVersion = SETUP_VERSION;
2426
- try {
2427
- const resp = await fetch('https://registry.npmjs.org/create-openclaw-bot/latest', { signal: AbortSignal.timeout(3000) });
2428
- if (resp.ok) {
2429
- const data = await resp.json();
2430
- if (data.version) latestSetupVersion = data.version;
2431
- }
2432
- } catch (e) {}
2494
+ fetchLatestSetupVersionBg().catch(() => {});
2495
+ const latestSetupVersion = latestSetupVersionCache;
2433
2496
 
2434
2497
  return json(res, {
2435
2498
  os: osChoice,
@@ -2544,6 +2607,7 @@ async function handler(req, res, rootProjectDir) {
2544
2607
  await run('npm', ['install'], { cwd: rootProjectDir });
2545
2608
  await run('npm', ['run', 'build'], { cwd: rootProjectDir });
2546
2609
  sendLog('[update-setup] Setup Wizard updated successfully! Please restart the installer.');
2610
+ restartInstaller();
2547
2611
  } catch (err) {
2548
2612
  sendLog(`[update-setup] Error updating: ${err.message}`);
2549
2613
  }
@@ -2555,6 +2619,7 @@ async function handler(req, res, rootProjectDir) {
2555
2619
  try {
2556
2620
  await run('npm', ['install', '-g', 'create-openclaw-bot@latest'], { cwd: rootProjectDir });
2557
2621
  sendLog('[update-setup] Setup Wizard updated successfully! Please restart the installer.');
2622
+ restartInstaller();
2558
2623
  } catch (err) {
2559
2624
  sendLog(`[update-setup] Error updating: ${err.message}`);
2560
2625
  }
@@ -2652,6 +2717,8 @@ async function handler(req, res, rootProjectDir) {
2652
2717
  skills: [
2653
2718
  { name: 'Browser', slug: 'browser' },
2654
2719
  { name: 'Cron', slug: 'cron' },
2720
+ { name: 'Tạo ảnh Infographic', slug: 'image-gen' },
2721
+ { name: 'Web Search', slug: 'web-search' },
2655
2722
  ],
2656
2723
  plugins: [
2657
2724
  { name: 'openclaw-browser-automation', package: 'openclaw-browser-automation' },
@@ -2700,13 +2767,42 @@ function openUrl(url) {
2700
2767
  child.unref();
2701
2768
  }
2702
2769
 
2770
+ function restartInstaller() {
2771
+ sendLog('[update-setup] Restarting Setup Wizard to apply update...');
2772
+ setTimeout(() => {
2773
+ try {
2774
+ if (activeServerInstance) {
2775
+ activeServerInstance.close();
2776
+ }
2777
+
2778
+ const entryFile = process.argv[1];
2779
+ const args = process.argv.slice(2);
2780
+
2781
+ if (!args.includes('--no-open')) {
2782
+ args.push('--no-open');
2783
+ }
2784
+
2785
+ const child = spawn(process.argv[0], [entryFile, ...args], {
2786
+ detached: true,
2787
+ stdio: 'inherit',
2788
+ shell: process.platform === 'win32'
2789
+ });
2790
+ child.unref();
2791
+
2792
+ process.exit(0);
2793
+ } catch (err) {
2794
+ sendLog(`[update-setup] Failed to restart: ${err.message}`);
2795
+ }
2796
+ }, 2000);
2797
+ }
2798
+
2703
2799
  export async function startLocalInstaller({ host = '127.0.0.1', preferredPort = 51789, openBrowser = true, projectDir = process.cwd() } = {}) {
2704
2800
  const port = await findPort(host, preferredPort);
2705
2801
  const server = http.createServer((req, res) => handler(req, res, projectDir));
2802
+ activeServerInstance = server;
2706
2803
  await new Promise((resolve) => server.listen(port, host, resolve));
2707
2804
  const url = `http://${host}:${port}`;
2708
2805
  console.log(`OpenClaw Setup UI: ${url}`);
2709
- console.log('Legacy CLI: create-openclaw-bot legacy');
2710
2806
  if (openBrowser) openUrl(url);
2711
2807
  }
2712
2808