create-openclaw-bot 5.8.2 → 5.8.4

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.
@@ -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');
@@ -27,6 +27,7 @@ const STATE_FILE = '.openclaw-setup-state.json';
27
27
  const DEFAULT_MODEL = 'smart-route';
28
28
  const logClients = new Set();
29
29
  let zaloLoginInFlight = false;
30
+ let activeServerInstance = null;
30
31
  const state = {
31
32
  installing: false,
32
33
  installed: false,
@@ -74,6 +75,30 @@ function detectOs() {
74
75
  return 'linux-desktop';
75
76
  }
76
77
 
78
+ // Blacklist of Windows system/large directories that should never be walked
79
+ const SYSTEM_DIR_BLACKLIST = new Set([
80
+ 'windows', 'program files', 'program files (x86)', 'programdata',
81
+ '$recycle.bin', 'system volume information', 'recovery', 'boot',
82
+ 'perflogs', 'msocache', 'intel', 'amd', 'nvidia',
83
+ '$windows.~bt', '$windows.~ws', 'config.msi', 'documents and settings',
84
+ 'swapfile.sys', 'pagefile.sys', 'hiberfil.sys',
85
+ ]);
86
+
87
+ /** Discover all available drive letters on Windows (A-Z). Returns ['C:\\', 'D:\\', ...] */
88
+ async function getAvailableDrives() {
89
+ if (process.platform !== 'win32') return ['/'];
90
+ const drives = [];
91
+ for (let code = 65; code <= 90; code++) { // A-Z
92
+ const letter = String.fromCharCode(code);
93
+ const drive = `${letter}:\\`;
94
+ try {
95
+ await fsp.access(drive);
96
+ drives.push(drive);
97
+ } catch {}
98
+ }
99
+ return drives.length ? drives : ['C:\\', 'D:\\'];
100
+ }
101
+
77
102
  function recommendedMode(osChoice) {
78
103
  if (osChoice === 'win' || osChoice === 'macos') return 'docker';
79
104
  return 'native';
@@ -566,6 +591,26 @@ function ensureZaloApiChannel(cfg, token) {
566
591
  });
567
592
  }
568
593
 
594
+ function ensureZaloModPluginConfig(entry, cfg) {
595
+ entry.hooks = entry.hooks || {};
596
+ entry.hooks.allowConversationAccess = true;
597
+ entry.config = entry.config || {};
598
+ // Auto-assign dashboardPort = gateway port + 1
599
+ if (!entry.config.dashboardPort) {
600
+ const gwPort = Number(cfg.gateway?.port) || state.gatewayPort || 18789;
601
+ entry.config.dashboardPort = gwPort + 1;
602
+ }
603
+ // Auto-assign botName from first agent name
604
+ if (!entry.config.botName) {
605
+ const agentName = cfg.agents?.list?.[0]?.name;
606
+ if (agentName) entry.config.botName = agentName;
607
+ }
608
+ // Auto-assign zaloDisplayNames from botName
609
+ if ((!entry.config.zaloDisplayNames || entry.config.zaloDisplayNames.length === 0) && entry.config.botName) {
610
+ entry.config.zaloDisplayNames = [entry.config.botName];
611
+ }
612
+ }
613
+
569
614
  function readProjectConfig(projectDir) {
570
615
  const cfgPath = join(projectDir || '', '.openclaw', 'openclaw.json');
571
616
  if (!projectDir || !existsSync(cfgPath)) return null;
@@ -857,7 +902,32 @@ async function buildBotStatus() {
857
902
  resolveProjectRuntimeVersions(state.projectDir, state.mode).catch(() => ({ openclaw: '', nineRouter: '', node: process.version || '' })),
858
903
  ]);
859
904
  const credentials = await readBotCredentials(state.projectDir).catch(() => ({ openclawToken: '', nineRouterApiKey: '' }));
860
- return { ...state, gatewayStatus, routerStatus, bots, credentials, runtimeVersions };
905
+
906
+ let activeModel = 'smart-route';
907
+ let activeProvider = '9Router';
908
+ if (state.projectDir) {
909
+ const cfgPath = join(state.projectDir, '.openclaw', 'openclaw.json');
910
+ if (existsSync(cfgPath)) {
911
+ try {
912
+ const raw = await fsp.readFile(cfgPath, 'utf8');
913
+ const cfg = JSON.parse(raw);
914
+ const modelStr = cfg.agents?.defaults?.model?.primary || cfg.agents?.list?.[0]?.model?.primary || 'smart-route';
915
+ if (modelStr.includes('/')) {
916
+ const parts = modelStr.split('/');
917
+ activeProvider = parts[0];
918
+ activeModel = parts.slice(1).join('/');
919
+ } else {
920
+ activeModel = modelStr;
921
+ activeProvider = cfg.models?.providers?.openai ? 'openai' : '9router';
922
+ }
923
+ } catch (e) {}
924
+ }
925
+ }
926
+
927
+ const cap = (s) => String(s).toLowerCase() === 'openai' ? 'OpenAI' : String(s).toLowerCase() === '9router' ? '9Router' : s;
928
+ activeProvider = cap(activeProvider);
929
+
930
+ return { ...state, gatewayStatus, routerStatus, bots, credentials, runtimeVersions, activeModel, activeProvider };
861
931
  }
862
932
 
863
933
  async function createBotInProject(projectDir, body = {}, runtime = {}) {
@@ -889,10 +959,16 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
889
959
  agentMetas: [],
890
960
  }));
891
961
 
892
- const existingAgentCount = cfg.agents.list.length;
893
962
  const used = new Set(cfg.agents.list.map((a) => a.id));
894
963
  const botName = uniqueDisplayName(requestedBotName, new Set(cfg.agents.list.map((a) => a.name || a.id)));
895
- const agentId = uniqueSlug(slugify(botName), used);
964
+ let agentId = body.agentId ? String(body.agentId).trim().toLowerCase().replace(/[^a-z0-9-_]+/g, '-') : '';
965
+ if (!agentId) {
966
+ agentId = uniqueSlug(slugify(botName), used);
967
+ } else {
968
+ if (used.has(agentId)) {
969
+ throw httpError(400, `Bot ID "${agentId}" đã tồn tại. Vui lòng chọn ID khác.`);
970
+ }
971
+ }
896
972
  const workspaceDir = `workspace-${agentId}`;
897
973
  const model = cfg.agents.defaults?.model?.primary || cfg.agents.list[0]?.model?.primary || DEFAULT_MODEL;
898
974
  cfg.agents.list.push({
@@ -935,6 +1011,8 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
935
1011
  if (existsSync(cfgPath)) await fsp.copyFile(cfgPath, `${cfgPath}.bak`);
936
1012
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
937
1013
 
1014
+ const hasScheduler = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
1015
+ const hasImageGen = !!(cfg.skills?.entries?.['image-gen']?.enabled);
938
1016
  const files = buildWorkspaceFileMap({
939
1017
  isVi: true,
940
1018
  botName,
@@ -945,6 +1023,8 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
945
1023
  agentWorkspaceDir: workspaceDir,
946
1024
  workspacePath: `.openclaw/${workspaceDir}`,
947
1025
  hasZaloMod: channel === 'zalo-personal',
1026
+ hasScheduler,
1027
+ hasImageGen,
948
1028
  });
949
1029
  const wsRoot = join(openclawHome, workspaceDir);
950
1030
  for (const [name, content] of Object.entries(files)) {
@@ -1033,6 +1113,8 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1033
1113
  }
1034
1114
  }
1035
1115
 
1116
+ const hasScheduler = !!(cfg.tools?.alsoAllow || []).includes('group:automation');
1117
+ const hasImageGen = !!(cfg.skills?.entries?.['image-gen']?.enabled);
1036
1118
  const files = buildWorkspaceFileMap({
1037
1119
  isVi: true,
1038
1120
  botName,
@@ -1043,6 +1125,8 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1043
1125
  agentWorkspaceDir: workspaceDir,
1044
1126
  workspacePath: `.openclaw/${workspaceDir}`,
1045
1127
  hasZaloMod: channel === 'zalo-personal',
1128
+ hasScheduler,
1129
+ hasImageGen,
1046
1130
  });
1047
1131
  const wsRoot = join(projectDir, '.openclaw', workspaceDir);
1048
1132
  for (const [name, content] of Object.entries(files)) {
@@ -1388,6 +1472,23 @@ async function syncDockerInfra(projectDir, force = false) {
1388
1472
  sendLog(`[sync] Updating Docker infrastructure files (v${existingVersion} \u2192 v${SETUP_VERSION})`);
1389
1473
  await fsp.writeFile(join(dockerDir, 'Dockerfile'), docker.dockerfile, 'utf8');
1390
1474
  await fsp.writeFile(join(dockerDir, 'docker-compose.yml'), newCompose, 'utf8');
1475
+ // Preserve zalo-mod dashboard port if plugin is active
1476
+ try {
1477
+ const syncCfg = JSON.parse(await fsp.readFile(cfgPath, 'utf8'));
1478
+ const zmEntry = syncCfg.plugins?.entries?.['zalo-mod'] || syncCfg.plugins?.entries?.['openclaw-zalo-mod'];
1479
+ if (zmEntry?.enabled !== false && zmEntry?.config?.dashboardPort) {
1480
+ const dp = zmEntry.config.dashboardPort;
1481
+ let cc = await fsp.readFile(join(dockerDir, 'docker-compose.yml'), 'utf8');
1482
+ if (!cc.includes(`:${dp}`)) {
1483
+ const gpStr = String(gatewayPort);
1484
+ cc = cc.replace(
1485
+ new RegExp(`^(\\s*-\\s*"(?:\\d+:)?${gpStr}(?::${gpStr})?"\\s*)$`, 'm'),
1486
+ `$1\n - "127.0.0.1:${dp}:${dp}" # zalo-mod dashboard`
1487
+ );
1488
+ await fsp.writeFile(join(dockerDir, 'docker-compose.yml'), cc, 'utf8');
1489
+ }
1490
+ }
1491
+ } catch {}
1391
1492
  await fsp.writeFile(entrypointPath, entryScript, 'utf8');
1392
1493
  if (docker.syncScript) await fsp.writeFile(join(dockerDir, 'sync.js'), docker.syncScript, 'utf8');
1393
1494
  if (docker.patchScript) await fsp.writeFile(join(dockerDir, 'patch-9router.js'), docker.patchScript, 'utf8');
@@ -1466,10 +1567,9 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
1466
1567
  await fsp.mkdir(openclawHome, { recursive: true });
1467
1568
  await fsp.mkdir(join(openclawHome, 'plugin-runtime-deps'), { recursive: true });
1468
1569
 
1469
- const selectedSkills = [];
1470
- const botName = 'OpenClaw Bot';
1471
- const agentMetas = [{ agentId: 'bot', name: botName, description: 'Personal OpenClaw assistant' }];
1472
- const common = { botName, channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
1570
+ const selectedSkills = ['memory', 'image-gen', 'web-search'];
1571
+ const agentMetas = [];
1572
+ const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
1473
1573
  const cfg = buildOpenclawJson(common);
1474
1574
  const env = buildEnvFileContent({ ...common, apiKey: '', botToken: '' });
1475
1575
  const approvals = buildExecApprovalsJson({ agentMetas });
@@ -1478,26 +1578,6 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
1478
1578
  await fsp.writeFile(join(projectDir, '.env'), env, 'utf8');
1479
1579
  await fsp.writeFile(join(openclawHome, 'exec-approvals.json'), JSON.stringify(approvals, null, 2), 'utf8');
1480
1580
 
1481
- const workspaceDir = 'workspace-bot';
1482
- const workspace = buildWorkspaceFileMap({
1483
- isVi: true,
1484
- botName,
1485
- channelKey: 'telegram',
1486
- providerKey: '9router',
1487
- selectedSkills,
1488
- skillsCatalog: dataExport.SKILLS || [],
1489
- agentMetas,
1490
- deployMode: mode,
1491
- osChoice,
1492
- agentWorkspaceDir: workspaceDir,
1493
- workspacePath: `.openclaw/${workspaceDir}`,
1494
- });
1495
- const wsRoot = join(openclawHome, workspaceDir);
1496
- for (const [name, content] of Object.entries(workspace)) {
1497
- await fsp.mkdir(dirname(join(wsRoot, name)), { recursive: true });
1498
- await fsp.writeFile(join(wsRoot, name), content || '', 'utf8');
1499
- }
1500
-
1501
1581
  if (mode === 'docker') {
1502
1582
  const projectName = slugify(basename(projectDir)) || 'bot';
1503
1583
  const docker = buildDockerArtifacts({
@@ -1551,7 +1631,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
1551
1631
  await fsp.mkdir(dockerDir, { recursive: true });
1552
1632
  const envContent = existsSync(rootEnvPath)
1553
1633
  ? await fsp.readFile(rootEnvPath, 'utf8')
1554
- : 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: '' });
1634
+ : buildEnvFileContent({ channelKey: 'telegram', providerKey: '9router', deployMode: mode, osChoice, selectedSkills: [], skills: dataExport.SKILLS || [], agentMetas: [], apiKey: '', botToken: '' });
1555
1635
  await fsp.writeFile(dockerEnvPath, envContent, 'utf8');
1556
1636
  sendLog(`Docker env ready: ${dockerEnvPath}`);
1557
1637
  await run('docker', ['compose', 'up', '-d', '--build'], { cwd: dockerDir });
@@ -1678,11 +1758,16 @@ async function findLatestProject(rootProjectDir) {
1678
1758
  join(rootProjectDir, DEFAULT_PROJECT_NAME),
1679
1759
  dirname(rootProjectDir),
1680
1760
  os.homedir(),
1681
- 'D:\\tmp',
1682
1761
  ];
1683
- for (const drive of ['D:\\', 'E:\\']) {
1762
+ // Scan all available drives, walking top-level dirs but skipping system folders
1763
+ const drives = await getAvailableDrives();
1764
+ for (const drive of drives) {
1684
1765
  const entries = await fsp.readdir(drive, { withFileTypes: true }).catch(() => []);
1685
- for (const e of entries) if (e.isDirectory() && !e.name.startsWith('$')) roots.push(join(drive, e.name));
1766
+ for (const e of entries) {
1767
+ if (e.isDirectory() && !e.name.startsWith('$') && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
1768
+ roots.push(join(drive, e.name));
1769
+ }
1770
+ }
1686
1771
  }
1687
1772
  const candidates = [];
1688
1773
  async function walk(dir, depth = 0) {
@@ -1693,7 +1778,11 @@ async function findLatestProject(rootProjectDir) {
1693
1778
  return;
1694
1779
  }
1695
1780
  const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
1696
- for (const e of entries) if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') await walk(join(dir, e.name), depth + 1);
1781
+ for (const e of entries) {
1782
+ if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && !SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) {
1783
+ await walk(join(dir, e.name), depth + 1);
1784
+ }
1785
+ }
1697
1786
  }
1698
1787
  for (const r of roots) await walk(r);
1699
1788
  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -1706,10 +1795,10 @@ async function discoverProjects(rootProjectDir) {
1706
1795
  rootProjectDir,
1707
1796
  dirname(rootProjectDir),
1708
1797
  process.env.OPENCLAW_HOME ? dirname(process.env.OPENCLAW_HOME) : '',
1709
- 'D:\\tmp',
1710
- 'D:\\',
1711
- 'E:\\',
1712
1798
  ];
1799
+ // Add all available drives for scanning
1800
+ const drives = await getAvailableDrives();
1801
+ for (const drive of drives) roots.push(drive);
1713
1802
  const seen = new Set();
1714
1803
  const hits = [];
1715
1804
  async function walk(dir, depth = 0) {
@@ -1743,7 +1832,7 @@ async function discoverProjects(rootProjectDir) {
1743
1832
  const entries = await fsp.readdir(full, { withFileTypes: true }).catch(() => []);
1744
1833
  for (const e of entries) {
1745
1834
  if (!e.isDirectory()) continue;
1746
- if (e.name === 'node_modules' || e.name.startsWith('.git')) continue;
1835
+ if (e.name === 'node_modules' || e.name.startsWith('.git') || SYSTEM_DIR_BLACKLIST.has(e.name.toLowerCase())) continue;
1747
1836
  await walk(join(full, e.name), depth + 1);
1748
1837
  }
1749
1838
  }
@@ -1897,72 +1986,23 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
1897
1986
  const k = `${kind}:${id}`;
1898
1987
 
1899
1988
  if (kind === 'skill' && id === 'browser') {
1989
+ delete cfg.browser;
1990
+ cfg.plugins = cfg.plugins || { entries: {} };
1991
+ cfg.plugins.entries = cfg.plugins.entries || {};
1992
+ const aliases = ['browser-automation', 'openclaw-browser-automation'];
1993
+ const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
1994
+ cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
1995
+ cfg.plugins.entries[existingKey].enabled = !!enabled;
1996
+ cfg.plugins.allow = cfg.plugins.allow || [];
1900
1997
  if (enabled) {
1901
- cfg.browser = {
1902
- enabled: true,
1903
- defaultProfile: 'host-chrome',
1904
- profiles: { 'host-chrome': { cdpUrl: 'http://127.0.0.1:9222', color: '#4285F4' } },
1905
- };
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
- }
1998
+ if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
1952
1999
  } else {
1953
- delete cfg.browser;
2000
+ cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== existingKey);
1954
2001
  for (const a of cfg.agents.list) {
1955
2002
  const bf = await readWorkspaceText(projectDir, a, 'BROWSER.md');
1956
2003
  if (existsSync(bf.file)) await fsp.rm(bf.file, { force: true });
1957
2004
  const bt = await readWorkspaceText(projectDir, a, 'browser-tool.js');
1958
2005
  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
2006
  }
1967
2007
  }
1968
2008
 
@@ -1985,32 +2025,17 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
1985
2025
  cfg.tools.alsoAllow = Array.from(new Set([...(cfg.tools.alsoAllow || []), 'group:automation']));
1986
2026
  cfg.commands = cfg.commands || {};
1987
2027
  cfg.commands.ownerAllowFrom = Array.from(new Set([...(cfg.commands.ownerAllowFrom || []), '*']));
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 là \`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
2028
  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');
2029
+ const sf = await readWorkspaceText(projectDir, a, 'skills/cronjob/SKILL.md');
2030
+ await fsp.mkdir(dirname(sf.file), { recursive: true });
2031
+ await fsp.writeFile(sf.file, buildCronjobSkillMd(true), 'utf8');
2007
2032
  }
2008
2033
  } else {
2009
2034
  if (cfg.tools?.alsoAllow) cfg.tools.alsoAllow = cfg.tools.alsoAllow.filter((x) => x !== 'group:automation');
2010
2035
  if (cfg.commands?.ownerAllowFrom) cfg.commands.ownerAllowFrom = cfg.commands.ownerAllowFrom.filter((x) => x !== '*');
2011
2036
  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');
2037
+ const sf = await readWorkspaceText(projectDir, a, 'skills/cronjob/SKILL.md');
2038
+ if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
2014
2039
  }
2015
2040
  }
2016
2041
 
@@ -2025,6 +2050,59 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2025
2050
  }
2026
2051
  }
2027
2052
 
2053
+ if (kind === 'skill' && id === 'image-gen') {
2054
+ cfg.skills = cfg.skills || { entries: {} };
2055
+ cfg.skills.entries = cfg.skills.entries || {};
2056
+ cfg.skills.entries['image-gen'] = cfg.skills.entries['image-gen'] || {};
2057
+ cfg.skills.entries['image-gen'].enabled = !!enabled;
2058
+
2059
+ for (const a of cfg.agents.list) {
2060
+ const sf = await readWorkspaceText(projectDir, a, 'skills/infographic-generator/SKILL.md');
2061
+ const js = await readWorkspaceText(projectDir, a, 'skills/infographic-generator/image-generator.js');
2062
+ if (enabled) {
2063
+ await fsp.mkdir(dirname(sf.file), { recursive: true });
2064
+ await fsp.writeFile(sf.file, buildInfographicGeneratorSkillMd(), 'utf8');
2065
+ await fsp.writeFile(js.file, buildInfographicGeneratorJs(), 'utf8');
2066
+ } else {
2067
+ if (existsSync(sf.file)) await fsp.rm(sf.file, { force: true });
2068
+ if (existsSync(js.file)) await fsp.rm(js.file, { force: true });
2069
+ }
2070
+ }
2071
+
2072
+ // Write cfgPath early so recreation reads updated openclaw.json
2073
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2074
+
2075
+ // Recreate container to apply updated openclaw.json
2076
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2077
+ if (hasDocker) {
2078
+ sendLog(`[docker] Infographic skill toggled to ${enabled}. Recreating containers...`);
2079
+ await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
2080
+ }
2081
+ }
2082
+
2083
+ if (kind === 'skill' && id === 'web-search') {
2084
+ cfg.plugins = cfg.plugins || { entries: {} };
2085
+ cfg.plugins.entries = cfg.plugins.entries || {};
2086
+ cfg.plugins.entries['duckduckgo'] = cfg.plugins.entries['duckduckgo'] || {};
2087
+ cfg.plugins.entries['duckduckgo'].enabled = !!enabled;
2088
+ cfg.plugins.allow = cfg.plugins.allow || [];
2089
+ if (enabled) {
2090
+ if (!cfg.plugins.allow.includes('duckduckgo')) cfg.plugins.allow.push('duckduckgo');
2091
+ } else {
2092
+ cfg.plugins.allow = cfg.plugins.allow.filter((x) => x !== 'duckduckgo');
2093
+ }
2094
+
2095
+ // Write cfgPath early so recreation reads updated openclaw.json
2096
+ await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2097
+
2098
+ // Recreate container to apply updated openclaw.json
2099
+ const hasDocker = existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'));
2100
+ if (hasDocker) {
2101
+ sendLog(`[docker] Web Search skill toggled to ${enabled}. Recreating containers...`);
2102
+ await recreateDockerBot(projectDir).catch((err) => sendLog(`[docker] Warning: Failed to recreate container: ${err.message}`));
2103
+ }
2104
+ }
2105
+
2028
2106
  if (kind === 'plugin') {
2029
2107
  cfg.plugins = cfg.plugins || { entries: {} };
2030
2108
  cfg.plugins.entries = cfg.plugins.entries || {};
@@ -2038,13 +2116,31 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
2038
2116
  const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
2039
2117
  cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
2040
2118
  cfg.plugins.entries[existingKey].enabled = !!enabled;
2041
- if (existingKey === 'zalo-mod') {
2042
- cfg.plugins.entries[existingKey].hooks = cfg.plugins.entries[existingKey].hooks || {};
2043
- cfg.plugins.entries[existingKey].hooks.allowConversationAccess = true;
2119
+ if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
2120
+ ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
2044
2121
  }
2045
2122
  // Only add the canonical config key to allow list (not all aliases)
2046
2123
  cfg.plugins.allow = cfg.plugins.allow || [];
2047
2124
  if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
2125
+ // Auto-expose zalo-mod dashboard port in docker-compose.yml when enabling
2126
+ if (enabled && (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod')) {
2127
+ const composeFile = join(projectDir, 'docker', 'openclaw', 'docker-compose.yml');
2128
+ if (existsSync(composeFile)) {
2129
+ try {
2130
+ let composeContent = await fsp.readFile(composeFile, 'utf8');
2131
+ const dashPort = cfg.plugins.entries[existingKey].config?.dashboardPort;
2132
+ if (dashPort && !composeContent.includes(`:${dashPort}`)) {
2133
+ const gwPortStr = String(Number(cfg.gateway?.port) || state.gatewayPort || 18789);
2134
+ composeContent = composeContent.replace(
2135
+ new RegExp(`^(\\s*-\\s*"(?:\\d+:)?${gwPortStr}(?::${gwPortStr})?"\\s*)$`, 'm'),
2136
+ `$1\n - "127.0.0.1:${dashPort}:${dashPort}" # zalo-mod dashboard`
2137
+ );
2138
+ await fsp.writeFile(composeFile, composeContent, 'utf8');
2139
+ sendLog(`[plugin] Added dashboard port ${dashPort} to docker-compose.yml`);
2140
+ }
2141
+ } catch (e) { sendLog(`[plugin] Warning: could not add dashboard port: ${e.message}`); }
2142
+ }
2143
+ }
2048
2144
  }
2049
2145
 
2050
2146
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
@@ -2062,8 +2158,15 @@ async function installFeature(projectDir, agentId, kind, id) {
2062
2158
 
2063
2159
  if (composeDir) {
2064
2160
  const botContainer = getBotContainerName(projectDir);
2161
+ sendLog(`[plugin] Installing/updating clawhub:${id} inside container ${botContainer}...`);
2065
2162
 
2066
- // 1. Temporarily disable the plugin in openclaw.json and restart container to unlock files
2163
+ const cmd = `cd /root/project && openclaw plugins install clawhub:${id} --force`;
2164
+ const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
2165
+
2166
+ if (cmdOut) {
2167
+ for (const line of `${cmdOut.stdout}\n${cmdOut.stderr}`.split(/\r?\n/).filter(Boolean)) sendLog(line);
2168
+ }
2169
+
2067
2170
  const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
2068
2171
  const pluginAliasMap = {
2069
2172
  'openclaw-browser-automation': ['browser-automation', 'openclaw-browser-automation'],
@@ -2072,53 +2175,12 @@ async function installFeature(projectDir, agentId, kind, id) {
2072
2175
  'openclaw-n8n-facebook-poster': ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
2073
2176
  };
2074
2177
  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
-
2094
- sendLog(`[plugin] Installing clawhub:${id} inside container ${botContainer}...`);
2095
-
2096
- let installSuccess = true;
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
2178
 
2104
2179
  if (cmdOut.code !== 0) {
2105
2180
  const folderExists = aliases.some((a) => existsSync(join(projectDir, '.openclaw', 'extensions', a)));
2106
2181
  if (folderExists) {
2107
2182
  sendLog(`[plugin] Warning: installation reported errors, but plugin folder successfully written. Proceeding.`);
2108
2183
  } else {
2109
- installSuccess = false;
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
2184
  throw new Error(cmdOut.stderr || cmdOut.stdout || `Failed to install plugin ${id} inside container.`);
2123
2185
  }
2124
2186
  }
@@ -2127,25 +2189,11 @@ async function installFeature(projectDir, agentId, kind, id) {
2127
2189
  const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
2128
2190
  cfg.plugins = cfg.plugins || { entries: {} };
2129
2191
  cfg.plugins.entries = cfg.plugins.entries || {};
2130
- const pluginAliasMap = {
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'],
2135
- };
2136
- const aliases = pluginAliasMap[id] || [id];
2137
2192
  const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
2138
2193
  cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
2139
2194
  cfg.plugins.entries[existingKey].enabled = true;
2140
- if (existingKey === 'zalo-mod') {
2141
- cfg.plugins.entries[existingKey].hooks = cfg.plugins.entries[existingKey].hooks || {};
2142
- cfg.plugins.entries[existingKey].hooks.allowConversationAccess = true;
2143
- // Auto-assign dashboard port = gateway port + 1 to avoid conflicts between bots
2144
- const gwPort = Number(cfg.gateway?.port) || state.gatewayPort || 18789;
2145
- cfg.plugins.entries[existingKey].config = cfg.plugins.entries[existingKey].config || {};
2146
- if (!cfg.plugins.entries[existingKey].config.dashboardPort) {
2147
- cfg.plugins.entries[existingKey].config.dashboardPort = gwPort + 1;
2148
- }
2195
+ if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
2196
+ ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
2149
2197
  }
2150
2198
  // Only add the canonical config key to allow list (not all aliases)
2151
2199
  if (!cfg.plugins.allow.includes(existingKey)) cfg.plugins.allow.push(existingKey);
@@ -2199,7 +2247,7 @@ async function installFeature(projectDir, agentId, kind, id) {
2199
2247
  sendLog(`[plugin] Installing clawhub:${id}...`);
2200
2248
 
2201
2249
  let installSuccess = true;
2202
- await run('openclaw', ['plugins', 'install', `clawhub:${id}`], {
2250
+ await run('openclaw', ['plugins', 'install', `clawhub:${id}`, '--force'], {
2203
2251
  cwd: projectDir,
2204
2252
  env: openclawProjectEnv(projectDir),
2205
2253
  resolveOnPattern: /Installed plugin:/
@@ -2230,15 +2278,8 @@ async function installFeature(projectDir, agentId, kind, id) {
2230
2278
  const existingKey = aliases.find((a) => cfg.plugins.entries[a]) || aliases[0];
2231
2279
  cfg.plugins.entries[existingKey] = cfg.plugins.entries[existingKey] || {};
2232
2280
  cfg.plugins.entries[existingKey].enabled = true;
2233
- if (existingKey === 'zalo-mod') {
2234
- cfg.plugins.entries[existingKey].hooks = cfg.plugins.entries[existingKey].hooks || {};
2235
- cfg.plugins.entries[existingKey].hooks.allowConversationAccess = true;
2236
- // Auto-assign dashboard port = gateway port + 1 to avoid conflicts between bots
2237
- const gwPort = Number(cfg.gateway?.port) || state.gatewayPort || 18789;
2238
- cfg.plugins.entries[existingKey].config = cfg.plugins.entries[existingKey].config || {};
2239
- if (!cfg.plugins.entries[existingKey].config.dashboardPort) {
2240
- cfg.plugins.entries[existingKey].config.dashboardPort = gwPort + 1;
2241
- }
2281
+ if (existingKey === 'zalo-mod' || existingKey === 'openclaw-zalo-mod') {
2282
+ ensureZaloModPluginConfig(cfg.plugins.entries[existingKey], cfg);
2242
2283
  }
2243
2284
  await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
2244
2285
  }
@@ -2302,6 +2343,8 @@ async function getFeatureFlags(projectDir, agentId = '') {
2302
2343
  Array.from(installedSpecs).some((spec) => spec.includes(a)) ||
2303
2344
  allowSet.has(a)
2304
2345
  );
2346
+ const imageGenOn = !!cfg.skills?.entries?.['image-gen']?.enabled;
2347
+ const webSearchOn = isEnabled(['duckduckgo']);
2305
2348
  const aliases = {
2306
2349
  browser: ['openclaw-browser-automation', 'browser-automation'],
2307
2350
  zalo: ['openclaw-zalo-mod', 'zalo-mod'],
@@ -2311,6 +2354,8 @@ async function getFeatureFlags(projectDir, agentId = '') {
2311
2354
  const flags = {
2312
2355
  'skill:browser': browserOn,
2313
2356
  'skill:cron': cronOn,
2357
+ 'skill:image-gen': imageGenOn,
2358
+ 'skill:web-search': webSearchOn,
2314
2359
  'plugin:openclaw-browser-automation': isEnabled(aliases.browser),
2315
2360
  'plugin:openclaw-zalo-mod': isEnabled(aliases.zalo),
2316
2361
  'plugin:openclaw-facebook-crawler': isEnabled(aliases.crawler),
@@ -2502,6 +2547,7 @@ async function handler(req, res, rootProjectDir) {
2502
2547
  await run('npm', ['install'], { cwd: rootProjectDir });
2503
2548
  await run('npm', ['run', 'build'], { cwd: rootProjectDir });
2504
2549
  sendLog('[update-setup] Setup Wizard updated successfully! Please restart the installer.');
2550
+ restartInstaller();
2505
2551
  } catch (err) {
2506
2552
  sendLog(`[update-setup] Error updating: ${err.message}`);
2507
2553
  }
@@ -2513,6 +2559,7 @@ async function handler(req, res, rootProjectDir) {
2513
2559
  try {
2514
2560
  await run('npm', ['install', '-g', 'create-openclaw-bot@latest'], { cwd: rootProjectDir });
2515
2561
  sendLog('[update-setup] Setup Wizard updated successfully! Please restart the installer.');
2562
+ restartInstaller();
2516
2563
  } catch (err) {
2517
2564
  sendLog(`[update-setup] Error updating: ${err.message}`);
2518
2565
  }
@@ -2610,6 +2657,8 @@ async function handler(req, res, rootProjectDir) {
2610
2657
  skills: [
2611
2658
  { name: 'Browser', slug: 'browser' },
2612
2659
  { name: 'Cron', slug: 'cron' },
2660
+ { name: 'Tạo ảnh Infographic', slug: 'image-gen' },
2661
+ { name: 'Web Search', slug: 'web-search' },
2613
2662
  ],
2614
2663
  plugins: [
2615
2664
  { name: 'openclaw-browser-automation', package: 'openclaw-browser-automation' },
@@ -2658,13 +2707,42 @@ function openUrl(url) {
2658
2707
  child.unref();
2659
2708
  }
2660
2709
 
2710
+ function restartInstaller() {
2711
+ sendLog('[update-setup] Restarting Setup Wizard to apply update...');
2712
+ setTimeout(() => {
2713
+ try {
2714
+ if (activeServerInstance) {
2715
+ activeServerInstance.close();
2716
+ }
2717
+
2718
+ const entryFile = process.argv[1];
2719
+ const args = process.argv.slice(2);
2720
+
2721
+ if (!args.includes('--no-open')) {
2722
+ args.push('--no-open');
2723
+ }
2724
+
2725
+ const child = spawn(process.argv[0], [entryFile, ...args], {
2726
+ detached: true,
2727
+ stdio: 'inherit',
2728
+ shell: process.platform === 'win32'
2729
+ });
2730
+ child.unref();
2731
+
2732
+ process.exit(0);
2733
+ } catch (err) {
2734
+ sendLog(`[update-setup] Failed to restart: ${err.message}`);
2735
+ }
2736
+ }, 2000);
2737
+ }
2738
+
2661
2739
  export async function startLocalInstaller({ host = '127.0.0.1', preferredPort = 51789, openBrowser = true, projectDir = process.cwd() } = {}) {
2662
2740
  const port = await findPort(host, preferredPort);
2663
2741
  const server = http.createServer((req, res) => handler(req, res, projectDir));
2742
+ activeServerInstance = server;
2664
2743
  await new Promise((resolve) => server.listen(port, host, resolve));
2665
2744
  const url = `http://${host}:${port}`;
2666
2745
  console.log(`OpenClaw Setup UI: ${url}`);
2667
- console.log('Legacy CLI: create-openclaw-bot legacy');
2668
2746
  if (openBrowser) openUrl(url);
2669
2747
  }
2670
2748