create-openclaw-bot 5.1.11 → 5.1.13

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/setup.js CHANGED
@@ -24,7 +24,7 @@
24
24
  };
25
25
 
26
26
  // ========== State ==========
27
- const state = {
27
+ const state = {
28
28
  currentStep: 1,
29
29
  totalSteps: 5,
30
30
  channel: null,
@@ -47,21 +47,21 @@
47
47
  apiKey: '',
48
48
  projectPath: '',
49
49
  },
50
- };
51
-
52
- function getGatewayAllowedOrigins(port) {
53
- const normalizedPort = Number(port) || 18791;
54
- const origins = new Set([
55
- `http://localhost:${normalizedPort}`,
56
- `http://127.0.0.1:${normalizedPort}`,
57
- `http://0.0.0.0:${normalizedPort}`,
58
- ]);
59
- const currentHost = (window.location && window.location.hostname) ? window.location.hostname.trim() : '';
60
- if (currentHost) {
61
- origins.add(`http://${currentHost}:${normalizedPort}`);
62
- }
63
- return Array.from(origins);
64
- }
50
+ };
51
+
52
+ function getGatewayAllowedOrigins(port) {
53
+ const normalizedPort = Number(port) || 18791;
54
+ const origins = new Set([
55
+ `http://localhost:${normalizedPort}`,
56
+ `http://127.0.0.1:${normalizedPort}`,
57
+ `http://0.0.0.0:${normalizedPort}`,
58
+ ]);
59
+ const currentHost = (window.location && window.location.hostname) ? window.location.hostname.trim() : '';
60
+ if (currentHost) {
61
+ origins.add(`http://${currentHost}:${normalizedPort}`);
62
+ }
63
+ return Array.from(origins);
64
+ }
65
65
 
66
66
 
67
67
  // ========== AI Providers & Models ==========
@@ -161,6 +161,14 @@
161
161
 
162
162
  // ========== Available Plugins (npm packages — runtime/channel extensions) ==========
163
163
  const PLUGINS = [
164
+ {
165
+ id: 'telegram-multibot-relay',
166
+ name: 'Telegram Multi-Bot Relay',
167
+ icon: '🤝',
168
+ descVi: 'Điều phối nhiều bot Telegram trong cùng group — tự động khi chọn nhiều bot', descEn: 'Coordinate multiple Telegram bots in one group — auto-selected with multi-bot',
169
+ package: 'telegram-multibot-relay',
170
+ hidden: true, // hidden in UI, auto-selected programmatically
171
+ },
164
172
  {
165
173
  id: 'voice-call',
166
174
  name: 'Voice Call',
@@ -193,7 +201,6 @@
193
201
 
194
202
  // ========== Available Skills (ClawHub registry — agent capabilities) ==========
195
203
  const SKILLS = [
196
- // Web Search removed — OpenClaw has native search built-in (no Tavily key needed)
197
204
  {
198
205
  id: 'browser',
199
206
  name: 'Browser Automation ⭐(Khuyên dùng)',
@@ -227,7 +234,7 @@
227
234
  id: 'image-gen',
228
235
  name: 'Image Generation',
229
236
  icon: '🎨',
230
- descVi: 'Tạo ảnh bằng AI (DALL·E, Flux...)', descEn: 'Generate images using AI (DALL-E, Flux...)',
237
+ descVi: 'Tạo ảnh bằng AI (DALL·E, Flux, Midjourney...)', descEn: 'Generate images via AI (DALL-E, Flux, Midjourney...)',
231
238
  slug: 'image-gen',
232
239
  noteVi: 'Dùng chung OPENAI_API_KEY (DALL-E) hoặc thêm FLUX_API_KEY', noteEn: 'Uses OPENAI_API_KEY (DALL-E) or FLUX_API_KEY',
233
240
  envVars: ['# FLUX_API_KEY=<your_flux_key> # chỉ cần nếu dùng Flux'],
@@ -243,11 +250,36 @@
243
250
  id: 'email',
244
251
  name: 'Email Assistant',
245
252
  icon: '📧',
246
- descVi: 'Quản lý, soạn, tóm tắt email', descEn: 'Manage, compose, summarize emails',
253
+ descVi: 'Quản lý, soạn, tóm tắt email (Gmail, Outlook...)', descEn: 'Manage, compose, summarize emails (Gmail, Outlook...)',
247
254
  slug: 'email-assistant',
248
255
  noteVi: 'Cần cấu hình SMTP trong .env', noteEn: 'Requires SMTP configuration in .env',
249
256
  envVars: ['SMTP_HOST=smtp.gmail.com', 'SMTP_PORT=587', 'SMTP_USER=<your_email>', 'SMTP_PASS=<your_app_password>'],
250
257
  },
258
+ {
259
+ id: 'web-search',
260
+ name: 'Web Search',
261
+ icon: '🔍',
262
+ descVi: 'Tìm kiếm web thời gian thực (DuckDuckGo) — không cần API key', descEn: 'Real-time web search (DuckDuckGo) — no API key needed',
263
+ slug: 'web-search',
264
+ },
265
+ {
266
+ id: 'notion',
267
+ name: 'Notion',
268
+ icon: '📓',
269
+ descVi: 'Tạo, chỉnh sửa trang và database Notion', descEn: 'Create and edit Notion pages and databases',
270
+ slug: 'notion',
271
+ noteVi: 'Cần Notion Integration Token', noteEn: 'Requires Notion Integration Token',
272
+ envVars: ['NOTION_API_KEY=<your_notion_integration_token>'],
273
+ },
274
+ {
275
+ id: 'slack',
276
+ name: 'Slack',
277
+ icon: '🗨️',
278
+ descVi: 'Gửi tin, react, ghim tin nhắn trong Slack', descEn: 'Send messages, react, pin items in Slack',
279
+ slug: 'slack',
280
+ noteVi: 'Cần Slack Bot Token', noteEn: 'Requires Slack Bot Token',
281
+ envVars: ['SLACK_BOT_TOKEN=<your_slack_bot_token>'],
282
+ },
251
283
  ];
252
284
 
253
285
  // ========== Channel definitions ==========
@@ -530,6 +562,24 @@
530
562
  state.bots.push({ name: '', slashCmd: '', desc: '', provider: 'google', model: 'google/gemini-2.5-flash', token: '', apiKey: '' });
531
563
  }
532
564
 
565
+ // Auto-select telegram-multibot-relay plugin when multi-bot, deselect when single
566
+ const relayId = 'telegram-multibot-relay';
567
+ if (count > 1) {
568
+ if (!state.config.plugins.includes(relayId)) {
569
+ state.config.plugins.push(relayId);
570
+ }
571
+ } else {
572
+ state.config.plugins = state.config.plugins.filter(p => p !== relayId);
573
+ }
574
+ // Sync relay card checkbox if already rendered
575
+ const relayCard = document.querySelector(`.plugin-card[data-plugin="${relayId}"]`);
576
+ if (relayCard) {
577
+ const isSelected = count > 1;
578
+ relayCard.classList.toggle('plugin-card--selected', isSelected);
579
+ const cb = relayCard.querySelector('input[type="checkbox"]');
580
+ if (cb) cb.checked = isSelected;
581
+ }
582
+
533
583
  // Show/hide group option for 2+ bots
534
584
  const groupOpt = document.getElementById('multibot-group-option');
535
585
  if (groupOpt) groupOpt.style.display = count > 1 ? '' : 'none';
@@ -606,10 +656,12 @@
606
656
  if (labelEl) labelEl.style.display = 'none';
607
657
  if (slashGroup) slashGroup.style.display = 'none';
608
658
 
609
- // Update fields
610
- const bot = state.bots[0] || { name: 'Bot 1', desc: '', persona: '', slashCmd: '' };
611
- document.getElementById('cfg-bot-tab-name').value = bot.name || '';
612
- document.getElementById('cfg-bot-tab-desc').value = bot.desc || '';
659
+ // Restore single-bot fields — fall back to state.config.botName so Next button
660
+ // is never falsely disabled just because state.bots[0].name is empty yet.
661
+ const bot = state.bots[0] || { name: '', desc: '', persona: '', slashCmd: '' };
662
+ const resolvedName = bot.name || state.config.botName || '';
663
+ document.getElementById('cfg-bot-tab-name').value = resolvedName;
664
+ document.getElementById('cfg-bot-tab-desc').value = bot.desc || state.config.description || '';
613
665
  document.getElementById('cfg-bot-tab-persona').value = bot.persona || '';
614
666
  return;
615
667
  }
@@ -640,9 +692,11 @@
640
692
  const nameEl = document.getElementById('cfg-bot-tab-name');
641
693
  const slashEl = document.getElementById('cfg-bot-tab-slash');
642
694
  const descEl = document.getElementById('cfg-bot-tab-desc');
695
+ const personaEl = document.getElementById('cfg-bot-tab-persona');
643
696
  if (nameEl) nameEl.value = bot.name || '';
644
697
  if (slashEl) slashEl.value = bot.slashCmd || '';
645
698
  if (descEl) descEl.value = bot.desc || '';
699
+ if (personaEl) personaEl.value = bot.persona || '';
646
700
 
647
701
  // Also sync global config fields from active bot (provider/model carry over)
648
702
  if (bot.provider) {
@@ -683,9 +737,11 @@
683
737
  const nameEl = document.getElementById('cfg-bot-tab-name');
684
738
  const slashEl = document.getElementById('cfg-bot-tab-slash');
685
739
  const descEl = document.getElementById('cfg-bot-tab-desc');
740
+ const personaEl = document.getElementById('cfg-bot-tab-persona');
686
741
  if (nameEl) bot.name = nameEl.value;
687
742
  if (slashEl) bot.slashCmd = slashEl.value;
688
743
  if (descEl) bot.desc = descEl.value;
744
+ if (personaEl) bot.persona = personaEl.value;
689
745
  }
690
746
 
691
747
  window.__saveBotTabName = function(val) {
@@ -711,6 +767,12 @@
711
767
  }
712
768
  };
713
769
 
770
+ window.__saveBotTabPersona = function(val) {
771
+ if (state.bots[state.activeBotIndex]) {
772
+ state.bots[state.activeBotIndex].persona = val;
773
+ }
774
+ };
775
+
714
776
 
715
777
 
716
778
  // ========== Step 1: Deploy Mode + OS ==========
@@ -922,19 +984,35 @@
922
984
  // Step 1 (env): always valid
923
985
  // Step 2 (channel): require selection
924
986
  if (state.currentStep === 2 && !state.channel) isDisabled = true;
925
- // Step 3 (bot config): require bot name
987
+ // Step 3 (bot config): require at least one bot name
926
988
  if (state.currentStep === 3) {
927
- const nameVal = document.getElementById('cfg-name')?.value?.trim();
928
- const userInfoVal = document.getElementById('cfg-user-info')?.value?.trim();
929
- if (!nameVal || !userInfoVal) isDisabled = true;
989
+ if (state.botCount > 1) {
990
+ // Multi-bot: require name for the currently active bot tab
991
+ // Fallback to state.bots to handle re-render cases where DOM may not yet have the value
992
+ const activeTab = state._activeBotTab || 0;
993
+ const tabNameVal = document.getElementById('cfg-bot-tab-name')?.value?.trim()
994
+ || state.bots[activeTab]?.name?.trim();
995
+ if (!tabNameVal) isDisabled = true;
996
+ } else {
997
+ // Single bot: require cfg-name or the shared tab name field
998
+ // Fallback to state.config.botName for cases where the DOM field was cleared on re-render
999
+ const nameVal = document.getElementById('cfg-name')?.value?.trim()
1000
+ || document.getElementById('cfg-bot-tab-name')?.value?.trim()
1001
+ || state.config.botName?.trim();
1002
+ if (!nameVal) isDisabled = true;
1003
+ }
930
1004
  }
931
1005
  // Step 4 (api keys): require token/key
932
1006
  if (state.currentStep === 4) {
933
- const botTokenEl = document.getElementById('key-bot-token');
934
1007
  const apiKeyEl = document.getElementById('key-api-key');
935
1008
  const provider = PROVIDERS[state.config.provider];
936
- if ((state.channel === 'telegram' || state.channel === 'zalo-bot') && botTokenEl) {
937
- if (!botTokenEl.value.trim()) isDisabled = true;
1009
+ if (state.channel === 'telegram' && state.botCount > 1) {
1010
+ // Multi-bot Telegram: require at least the first bot's token
1011
+ const firstTokenEl = document.getElementById('key-bot-token-0');
1012
+ if (!firstTokenEl || !firstTokenEl.value.trim()) isDisabled = true;
1013
+ } else if (state.channel === 'telegram' || state.channel === 'zalo-bot') {
1014
+ const botTokenEl = document.getElementById('key-bot-token');
1015
+ if (!botTokenEl || !botTokenEl.value.trim()) isDisabled = true;
938
1016
  }
939
1017
  if (provider && !provider.isProxy && !provider.isLocal && provider.envKey && apiKeyEl) {
940
1018
  if (!apiKeyEl.value.trim()) isDisabled = true;
@@ -1010,9 +1088,11 @@
1010
1088
  }
1011
1089
 
1012
1090
  // Plugins grid (npm packages — extra channels/extensions)
1091
+ // Filter out hidden plugins from user-facing grid
1092
+ const visiblePlugins = PLUGINS.filter((p) => !p.hidden);
1013
1093
  const pluginGrid = document.getElementById('extra-plugin-grid');
1014
1094
  if (pluginGrid) {
1015
- pluginGrid.innerHTML = PLUGINS.map((p) => `
1095
+ pluginGrid.innerHTML = visiblePlugins.map((p) => `
1016
1096
  <label class="plugin-card" data-plugin="${p.id}">
1017
1097
  <input type="checkbox" class="plugin-checkbox" value="${p.id}" onchange="window.__togglePlugin('${p.id}', this.checked)">
1018
1098
  <div class="plugin-card__icon">${p.icon}</div>
@@ -1065,6 +1145,17 @@
1065
1145
  prompt.value = DEFAULT_PROMPTS[lang].replace('{BOT_NAME}', nameVal).replace('{BOT_DESC}', descVal);
1066
1146
  autoExpand(prompt);
1067
1147
  }
1148
+ // Sync single-bot name to state + re-check Next button
1149
+ if (e.target.id === 'cfg-name') {
1150
+ state.config.botName = e.target.value;
1151
+ if (state.bots[0]) state.bots[0].name = e.target.value;
1152
+ }
1153
+ updateNavButtons();
1154
+ }
1155
+ // Also re-check Next when typing directly in the tab name field
1156
+ if (e.target.id === 'cfg-bot-tab-name') {
1157
+ if (state.bots[state.activeBotIndex]) state.bots[state.activeBotIndex].name = e.target.value;
1158
+ updateNavButtons();
1068
1159
  }
1069
1160
  if (e.target.id === 'cfg-prompt') {
1070
1161
  e.target.dataset.userEdited = 'true';
@@ -1164,6 +1255,12 @@
1164
1255
  state.config.systemPrompt = document.getElementById('cfg-prompt')?.value || state.config.systemPrompt || DEFAULT_PROMPTS['vi'];
1165
1256
  state.config.userInfo = document.getElementById('cfg-user-info')?.value?.trim() || state.config.userInfo || '';
1166
1257
  state.config.securityRules = document.getElementById('cfg-security')?.value || state.config.securityRules || DEFAULT_SECURITY_RULES['vi'];
1258
+ // Also save bot-tab-name → bots[0].name so both state locations stay in sync
1259
+ const tabName = document.getElementById('cfg-bot-tab-name')?.value?.trim();
1260
+ if (tabName && state.bots[0]) state.bots[0].name = tabName;
1261
+ else if (state.config.botName && state.bots[0] && !state.bots[0].name) {
1262
+ state.bots[0].name = state.config.botName;
1263
+ }
1167
1264
  }
1168
1265
 
1169
1266
  // Save Step 4 credential inputs to state (persists across Back navigation)
@@ -1591,15 +1688,15 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1591
1688
  commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
1592
1689
  channels: ch.channelConfig,
1593
1690
  tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
1594
- gateway: {
1595
- port: 18791,
1596
- mode: 'local',
1597
- bind: '0.0.0.0',
1598
- controlUi: {
1599
- allowedOrigins: getGatewayAllowedOrigins(18791),
1600
- },
1601
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
1602
- },
1691
+ gateway: {
1692
+ port: 18791,
1693
+ mode: 'local',
1694
+ bind: '0.0.0.0',
1695
+ controlUi: {
1696
+ allowedOrigins: getGatewayAllowedOrigins(18791),
1697
+ },
1698
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
1699
+ },
1603
1700
  };
1604
1701
 
1605
1702
  // 9Router: add proxy endpoint config under models.providers
@@ -1740,6 +1837,17 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1740
1837
  'telegram-multibot-relay': { enabled: true },
1741
1838
  },
1742
1839
  };
1840
+ } else if (state.config.plugins.length > 0) {
1841
+ // Non-multibot: write selected visible plugins into openclaw.json
1842
+ const pluginEntries = {};
1843
+ state.config.plugins.forEach((pid) => {
1844
+ const plugin = PLUGINS.find((p) => p.id === pid);
1845
+ if (!plugin || plugin.hidden) return;
1846
+ pluginEntries[plugin.package || pid] = { enabled: true };
1847
+ });
1848
+ if (Object.keys(pluginEntries).length > 0) {
1849
+ clawConfig.plugins = { entries: pluginEntries };
1850
+ }
1743
1851
  }
1744
1852
 
1745
1853
  setOutput('out-openclaw-json', JSON.stringify(clawConfig, null, 2));
@@ -1775,19 +1883,19 @@ model:
1775
1883
  // 3. Dockerfile
1776
1884
  const allPlugins = [];
1777
1885
  if (ch.pluginInstall) allPlugins.push(ch.pluginInstall);
1778
- const encodeBase64Utf8 = (value) => btoa(String.fromCharCode(...new TextEncoder().encode(String(value))));
1779
- const indentBlock = (text, spaces) => {
1780
- const prefix = ' '.repeat(spaces);
1781
- return String(text).split('\n').map((line) => `${prefix}${line}`).join('\n');
1782
- };
1783
- const build9RouterComposeEntrypointScript = (syncScriptBase64) => [
1784
- 'npm install -g 9router',
1785
- `node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
1786
- 'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
1787
- 'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
1788
- ].join('\n');
1789
-
1790
- state.config.plugins.forEach((pid) => {
1886
+ const encodeBase64Utf8 = (value) => btoa(String.fromCharCode(...new TextEncoder().encode(String(value))));
1887
+ const indentBlock = (text, spaces) => {
1888
+ const prefix = ' '.repeat(spaces);
1889
+ return String(text).split('\n').map((line) => `${prefix}${line}`).join('\n');
1890
+ };
1891
+ const build9RouterComposeEntrypointScript = (syncScriptBase64) => [
1892
+ 'npm install -g 9router',
1893
+ `node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
1894
+ 'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
1895
+ 'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
1896
+ ].join('\n');
1897
+
1898
+ state.config.plugins.forEach((pid) => {
1791
1899
  const plug = PLUGINS.find((p) => p.id === pid);
1792
1900
  if (plug) allPlugins.push(plug.package);
1793
1901
  });
@@ -1832,7 +1940,7 @@ model:
1832
1940
  ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
1833
1941
  : '';
1834
1942
  // Patch config on every startup to keep gateway settings stable
1835
- const patchCmd = `node -e \\"const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add(\\\`http://\\\${entry.address}:18791\\\`);}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
1943
+ const patchCmd = `node -e \\"const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add(\\\`http://\\\${entry.address}:18791\\\`);}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
1836
1944
  // Auto-approve device pairing after gateway starts (required since v2026.3.x)
1837
1945
  const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
1838
1946
  const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${autoApproveCmd}${gatewayCmd}"`;
@@ -1844,7 +1952,7 @@ RUN apt-get update && apt-get install -y git curl${browserAptExtra} && rm -rf /v
1844
1952
 
1845
1953
  ARG CACHEBUST=${Date.now()}
1846
1954
  RUN npm install -g openclaw@latest${skillLines}${browserInstallLines}
1847
- RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"
1955
+ RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"
1848
1956
  WORKDIR /root/.openclaw
1849
1957
 
1850
1958
  EXPOSE 18791
@@ -1863,7 +1971,7 @@ ${finalCmd}`;
1863
1971
  // Background loop inside 9Router container every 30s.
1864
1972
  // Read providerConnections directly from db.json so smart-route survives
1865
1973
  // dashboard auth/response changes in newer 9Router builds.
1866
- const syncScript = `const fs=require('fs');const INTERVAL=30000;const p='/root/.9router/db.json';
1974
+ const syncScript = `const fs=require('fs');const INTERVAL=30000;const p='/root/.9router/db.json';
1867
1975
  const PM={codex:['cx/gpt-5.4','cx/gpt-5.3-codex','cx/gpt-5.3-codex-high','cx/gpt-5.2-codex','cx/gpt-5.2','cx/gpt-5.1-codex-max','cx/gpt-5.1-codex','cx/gpt-5.1','cx/gpt-5-codex'],'claude-code':['cc/claude-opus-4-6','cc/claude-sonnet-4-6','cc/claude-opus-4-5-20251101','cc/claude-sonnet-4-5-20250929','cc/claude-haiku-4-5-20251001'],github:['gh/gpt-5.4','gh/gpt-5.3-codex','gh/gpt-5.2-codex','gh/gpt-5.2','gh/gpt-5.1-codex-max','gh/gpt-5.1-codex','gh/gpt-5.1','gh/gpt-5','gh/gpt-4.1','gh/gpt-4o','gh/claude-opus-4.6','gh/claude-sonnet-4.6','gh/claude-sonnet-4.5','gh/claude-opus-4.5','gh/claude-haiku-4.5','gh/gemini-3-pro-preview','gh/gemini-3-flash-preview','gh/gemini-2.5-pro'],cursor:['cu/default','cu/claude-4.6-opus-max','cu/claude-4.5-opus-high-thinking','cu/claude-4.5-sonnet-thinking','cu/claude-4.5-sonnet','cu/gpt-5.3-codex','cu/gpt-5.2-codex','cu/gemini-3-flash-preview'],kilo:['kc/anthropic/claude-sonnet-4-20250514','kc/anthropic/claude-opus-4-20250514','kc/google/gemini-2.5-pro','kc/google/gemini-2.5-flash','kc/openai/gpt-4.1','kc/deepseek/deepseek-chat'],cline:['cl/anthropic/claude-sonnet-4.6','cl/anthropic/claude-opus-4.6','cl/openai/gpt-5.3-codex','cl/openai/gpt-5.4','cl/google/gemini-3.1-pro-preview'],'gemini-cli':['gc/gemini-3-flash-preview','gc/gemini-3-pro-preview'],iflow:['if/qwen3-coder-plus','if/kimi-k2','if/kimi-k2-thinking','if/glm-4.7','if/deepseek-r1','if/deepseek-v3.2','if/deepseek-v3','if/qwen3-max','if/qwen3-235b','if/iflow-rome-30ba3b'],qwen:['qw/qwen3-coder-plus','qw/qwen3-coder-flash','qw/vision-model','qw/coder-model'],kiro:['kr/claude-sonnet-4.5','kr/claude-haiku-4.5','kr/deepseek-3.2','kr/deepseek-3.1','kr/qwen3-coder-next'],ollama:['ollama/gemma4:e2b','ollama/gemma4:e4b','ollama/gemma4:26b','ollama/gemma4:31b','ollama/qwen3.5','ollama/kimi-k2.5','ollama/glm-5','ollama/glm-4.7-flash','ollama/minimax-m2.5','ollama/gpt-oss:120b'],'kimi-coding':['kmc/kimi-k2.5','kmc/kimi-k2.5-thinking','kmc/kimi-latest'],glm:['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],'glm-cn':['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],minimax:['minimax/MiniMax-M2.7','minimax/MiniMax-M2.5','minimax/MiniMax-M2.1'],kimi:['kimi/kimi-k2.5','kimi/kimi-k2.5-thinking','kimi/kimi-latest'],deepseek:['deepseek/deepseek-chat','deepseek/deepseek-reasoner'],xai:['xai/grok-4','xai/grok-4-fast-reasoning','xai/grok-code-fast-1'],mistral:['mistral/mistral-large-latest','mistral/codestral-latest'],groq:['groq/llama-3.3-70b-versatile','groq/openai/gpt-oss-120b'],cerebras:['cerebras/gpt-oss-120b'],alicode:['alicode/qwen3.5-plus','alicode/qwen3-coder-plus'],openai:['openai/gpt-4o','openai/gpt-4.1'],anthropic:['anthropic/claude-sonnet-4','anthropic/claude-haiku-3.5'],gemini:['gemini/gemini-2.5-flash','gemini/gemini-2.5-pro']};
1868
1976
  console.log('[sync-combo] 9Router sync loop started...');
1869
1977
  const sync = async () => {
@@ -1913,11 +2021,11 @@ const sync = async () => {
1913
2021
  console.log('[sync-combo] Created smart-route: ' + c.models.length + ' models');
1914
2022
  }
1915
2023
  } catch (e) { }
1916
- };
1917
- setTimeout(sync, 5000);
1918
- setInterval(sync, INTERVAL);`;
1919
- const syncScriptBase64 = encodeBase64Utf8(syncScript);
1920
- const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncScriptBase64);
2024
+ };
2025
+ setTimeout(sync, 5000);
2026
+ setInterval(sync, INTERVAL);`;
2027
+ const syncScriptBase64 = encodeBase64Utf8(syncScript);
2028
+ const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncScriptBase64);
1921
2029
 
1922
2030
  let compose;
1923
2031
  if (isMultiBotWizard) {
@@ -1946,14 +2054,14 @@ ${dependsOn}${extraHosts} volumes:
1946
2054
  image: node:22-slim
1947
2055
  container_name: 9router-multibot
1948
2056
  restart: always
1949
- entrypoint:
1950
- - /bin/sh
1951
- - -c
1952
- - |
1953
- ${indentBlock(docker9RouterEntrypointScript, 8)}
1954
- environment:
1955
- - PORT=20128
1956
- - HOSTNAME=0.0.0.0
2057
+ entrypoint:
2058
+ - /bin/sh
2059
+ - -c
2060
+ - |
2061
+ ${indentBlock(docker9RouterEntrypointScript, 8)}
2062
+ environment:
2063
+ - PORT=20128
2064
+ - HOSTNAME=0.0.0.0
1957
2065
  - CI=true
1958
2066
  volumes:
1959
2067
  - 9router-data:/root/.9router
@@ -2039,14 +2147,14 @@ ${extraHostsBlock}
2039
2147
  image: node:22-slim
2040
2148
  container_name: 9router
2041
2149
  restart: always
2042
- entrypoint:
2043
- - /bin/sh
2044
- - -c
2045
- - |
2046
- ${indentBlock(docker9RouterEntrypointScript, 8)}
2047
- environment:
2048
- - PORT=20128
2049
- - HOSTNAME=0.0.0.0
2150
+ entrypoint:
2151
+ - /bin/sh
2152
+ - -c
2153
+ - |
2154
+ ${indentBlock(docker9RouterEntrypointScript, 8)}
2155
+ environment:
2156
+ - PORT=20128
2157
+ - HOSTNAME=0.0.0.0
2050
2158
  - CI=true
2051
2159
  volumes:
2052
2160
  - 9router-data:/root/.9router
@@ -2885,17 +2993,17 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
2885
2993
  // ─── Shared initializer (provider install) ───────────────────────────────
2886
2994
  function providerLines(arr, shell) {
2887
2995
  if (is9Router) {
2888
- if (shell === 'bat') {
2889
- arr.push('npm install -g 9router');
2890
- arr.push('start "9Router" cmd /k "9router -n -l -H 0.0.0.0 -p 20128 --skip-update"');
2891
- arr.push('start "9Router Smart Route Sync" cmd /k "node .\\.openclaw\\9router-smart-route-sync.js"');
2892
- arr.push('timeout /t 5 /nobreak >nul');
2893
- } else {
2894
- arr.push('npm install -g 9router');
2895
- arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 node "$(npm root -g)/9router/app/server.js" >/tmp/9router.log 2>&1 &');
2896
- arr.push('nohup node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
2897
- arr.push('sleep 3');
2898
- }
2996
+ if (shell === 'bat') {
2997
+ arr.push('npm install -g 9router');
2998
+ arr.push('start "9Router" cmd /k "9router -n -l -H 0.0.0.0 -p 20128 --skip-update"');
2999
+ arr.push('start "9Router Smart Route Sync" cmd /k "node .\\.openclaw\\9router-smart-route-sync.js"');
3000
+ arr.push('timeout /t 5 /nobreak >nul');
3001
+ } else {
3002
+ arr.push('npm install -g 9router');
3003
+ arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 node "$(npm root -g)/9router/app/server.js" >/tmp/9router.log 2>&1 &');
3004
+ arr.push('nohup node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
3005
+ arr.push('sleep 3');
3006
+ }
2899
3007
  } else if (isOllama) {
2900
3008
  if (shell === 'bat') {
2901
3009
  arr.push('where ollama >nul 2>&1 || (powershell -Command "Invoke-WebRequest -Uri https://ollama.com/download/OllamaSetup.exe -OutFile OllamaSetup.exe" && OllamaSetup.exe && del OllamaSetup.exe)');
@@ -3053,15 +3161,15 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3053
3161
  'telegram-multibot-relay': { enabled: true },
3054
3162
  },
3055
3163
  },
3056
- gateway: {
3057
- port: 18791,
3058
- mode: 'local',
3059
- bind: '0.0.0.0',
3060
- controlUi: {
3061
- allowedOrigins: getGatewayAllowedOrigins(18791),
3062
- },
3063
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3064
- },
3164
+ gateway: {
3165
+ port: 18791,
3166
+ mode: 'local',
3167
+ bind: '0.0.0.0',
3168
+ controlUi: {
3169
+ allowedOrigins: getGatewayAllowedOrigins(18791),
3170
+ },
3171
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3172
+ },
3065
3173
  };
3066
3174
  return JSON.stringify(cfg, null, 2);
3067
3175
  }
@@ -3103,9 +3211,11 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3103
3211
  files[`.openclaw/${meta.workspaceDir}/SOUL.md`] = isVi
3104
3212
  ? `# Tinh cach\n\n${meta.persona || 'Huu ich that su, gan gui, ngan gon.'}`
3105
3213
  : `# Soul\n\n${meta.persona || 'Helpful, concise, and practical.'}`;
3214
+ const _secRulesForBot = state.config.securityRules || DEFAULT_SECURITY_RULES[isVi ? 'vi' : 'en'];
3106
3215
  files[`.openclaw/${meta.workspaceDir}/AGENTS.md`] = (isVi
3107
- ? `# Huong dan van hanh\n\n## Vai tro\nBan la **${meta.name}**, ${meta.desc.toLowerCase()}.\n\n## Khi nao nen tra loi\n- Trong group, xem user dang goi ban neu tin nhan co alias cua ban: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}\n- Neu user tag username Telegram cua ban thi luon tra loi.\n- Gateway se tu dong tha ack \`👍\` khi nhan message; khong can tu tha them neu ack da hien.\n- Neu user dang goi ro bot khac ${relayTargetNames} thi khong cuop loi.\n\n## Phoi hop\n- Dung \`TEAM.md\` lam nguon su that.\n- Neu user bao ban hoi hoac xin y kien tu ${otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`'} thi dung agent-to-agent noi bo ngay trong turn hien tai.\n- Neu ban la bot mo loi, chi gui 1 cau mo dau ngan roi handoff ngay. Khong tu noi thay bot dich tru khi handoff that bai ro rang.\n- Khi handoff, phai goi dung agent id ky thuat ${relayTargetIds}, khong dung ten hien thi.\n- Neu ban la bot dich nhan handoff, hay tra loi cong khai ngay trong cung Telegram chat/thread bang chinh account cua minh. Uu tien tra loi co \`[[reply_to_current]]\`; neu can, dung Telegram send/sendMessage action thay vi chi output thuong.\n- Khong bao user phai tag lai bot kia neu ban co the hoi noi bo duoc.`
3108
- : `# Operating Manual\n\n## Role\nYou are **${meta.name}**, ${meta.desc.toLowerCase()}.\n\n## When To Reply\n- In group chats, treat the message as addressed to you if it contains one of your aliases: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}\n- Always reply when your Telegram username is tagged.\n- The gateway auto-sends the \`👍\` ack reaction on inbound messages; do not duplicate it manually if it already appeared.\n- If the message is clearly calling another bot such as ${relayTargetNames}, do not hijack it.\n\n## Coordination\n- Use \`TEAM.md\` as the source of truth.\n- If the user asks you to consult ${otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`another bot`'}, use internal agent-to-agent handoff in the same turn.\n- If you are the caller bot, send only one short opener then hand off immediately. Do not speak for the target bot unless the handoff clearly fails.\n- When handing off, use the exact technical agent id ${relayTargetIds}, not the display name.\n- If you are the target bot receiving a handoff, publish the real answer into the same Telegram chat/thread from your own account. Prefer replying with \`[[reply_to_current]]\`; if needed, use the Telegram send/sendMessage action instead of plain assistant output.\n- Do not ask the user to tag the other bot again if you can consult internally.`);
3216
+ ? `# Huong dan van hanh\n\n## Vai tro\nBan la **${meta.name}**, ${meta.desc.toLowerCase()}.\n\n## Khi nao nen tra loi\n- Trong group, xem user dang goi ban neu tin nhan co alias cua ban: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}\n- Neu user tag username Telegram cua ban thi luon tra loi.\n- Gateway se tu dong tha ack \`👍\` khi nhan message; khong can tu tha them neu ack da hien.\n- Neu user dang goi ro bot khac ${relayTargetNames} thi khong cuop loi.\n\n## Phoi hop\n- Dung \`TEAM.md\` lam nguon su that.\n- Neu user bao ban hoi hoac xin y kien tu ${otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`'} thi dung agent-to-agent noi bo ngay trong turn hien tai.\n- Neu ban la bot mo loi, chi gui 1 cau mo dau ngan roi handoff ngay. Khong tu noi thay bot dich tru khi handoff that bai ro rang.\n- Khi handoff, phai goi dung agent id ky thuat ${relayTargetIds}, khong dung ten hien thi.\n- Neu ban la bot dich nhan handoff, hay tra loi cong khai ngay trong cung Telegram chat/thread bang chinh account cua minh. Uu tien tra loi co \`[[reply_to_current]]\`; neu can, dung Telegram send/sendMessage action thay vi chi output thuong.\n- Khong bao user phai tag lai bot kia neu ban co the hoi noi bo duoc.\n\n${_secRulesForBot}`
3217
+ : `# Operating Manual\n\n## Role\nYou are **${meta.name}**, ${meta.desc.toLowerCase()}.\n\n## When To Reply\n- In group chats, treat the message as addressed to you if it contains one of your aliases: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}\n- Always reply when your Telegram username is tagged.\n- The gateway auto-sends the \`👍\` ack reaction on inbound messages; do not duplicate it manually if it already appeared.\n- If the message is clearly calling another bot such as ${relayTargetNames}, do not hijack it.\n\n## Coordination\n- Use \`TEAM.md\` as the source of truth.\n- If the user asks you to consult ${otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`another bot`'}, use internal agent-to-agent handoff in the same turn.\n- If you are the caller bot, send only one short opener then hand off immediately. Do not speak for the target bot unless the handoff clearly fails.\n- When handing off, use the exact technical agent id ${relayTargetIds}, not the display name.\n- If you are the target bot receiving a handoff, publish the real answer into the same Telegram chat/thread from your own account. Prefer replying with \`[[reply_to_current]]\`; if needed, use the Telegram send/sendMessage action instead of plain assistant output.\n- Do not ask the user to tag the other bot again if you can consult internally.\n\n${_secRulesForBot}`);
3218
+
3109
3219
  files[`.openclaw/${meta.workspaceDir}/TEAM.md`] = teamMd;
3110
3220
  files[`.openclaw/${meta.workspaceDir}/RELAY.md`] = isVi
3111
3221
  ? `# Telegram Relay Playbook\n\n## Muc tieu\n- Cho phep bot mo loi goi bot dich noi bo, sau do bot dich tra loi cong khai bang chinh account cua minh.\n\n## Protocol\n1. Bot mo loi gui 1 cau ngan xac nhan se hoi bot dich.\n2. Bot mo loi handoff noi bo bang dung agent id trong \`TEAM.md\`.\n3. Bot dich tra loi cong khai trong cung chat/thread hien tai.\n4. Neu thay \`[[reply_to_current]]\` hoac Telegram send/sendMessage action kha dung, uu tien dung de bam dung message goc.\n5. Neu handoff that bai ro rang, chi bot mo loi moi duoc fallback tom tat.\n`
@@ -3169,15 +3279,15 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3169
3279
  },
3170
3280
  commands: { native: 'auto', nativeSkills: 'auto', restart: true },
3171
3281
  channels: channelConfig,
3172
- gateway: {
3173
- port: basePort,
3174
- mode: 'local',
3175
- bind: '0.0.0.0',
3176
- controlUi: {
3177
- allowedOrigins: getGatewayAllowedOrigins(basePort),
3178
- },
3179
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3180
- },
3282
+ gateway: {
3283
+ port: basePort,
3284
+ mode: 'local',
3285
+ bind: '0.0.0.0',
3286
+ controlUi: {
3287
+ allowedOrigins: getGatewayAllowedOrigins(basePort),
3288
+ },
3289
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3290
+ },
3181
3291
 
3182
3292
  };
3183
3293
  return JSON.stringify(cfg, null, 2);
@@ -3509,29 +3619,55 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3509
3619
  } else if (state.nativeOs === 'linux') {
3510
3620
  const isDocker = state.deployMode === 'docker';
3511
3621
  scriptName = isDocker ? 'setup-openclaw-docker-macos.sh' : 'setup-openclaw-macos.sh';
3512
- const sh = [
3513
- '#!/usr/bin/env bash', 'set -e',
3514
- `echo "=== OpenClaw Setup — macOS${isDocker ? ' Docker' : ' Native'} ==="`,
3515
- 'command -v node > /dev/null 2>&1 || { echo "ERROR: Node.js chua cai! https://nodejs.org"; exit 1; }',
3516
- 'mkdir -p "$HOME/.local/bin"',
3517
- 'npm config set prefix "$HOME/.local"',
3518
- 'export PATH="$HOME/.local/bin:$PATH"',
3519
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.zshrc"',
3520
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3521
- 'npm install -g openclaw@latest',
3522
- ];
3523
- providerLines(sh, 'sh');
3524
- if (pluginCmd) sh.push(pluginCmd);
3525
3622
 
3526
- if (isMultiBot) {
3527
- appendShWriteCommands(sh, sharedNativeFileMap());
3528
- sh.push('echo "Starting shared multi-bot gateway..."');
3529
- sh.push('openclaw gateway run');
3530
- } else {
3623
+ if (isDocker) {
3624
+ // ── macOS Docker mode: write files then docker compose up ──────────────
3625
+ const sh = [
3626
+ '#!/usr/bin/env bash', 'set -e',
3627
+ 'echo "=== OpenClaw Setup \u2014 macOS Docker ==="',
3628
+ '# Check Docker Desktop is running',
3629
+ 'if ! docker info > /dev/null 2>&1; then',
3630
+ ' echo "\u274c Docker Desktop chua chay! Mo Docker Desktop roi chay lai script nay."',
3631
+ ' exit 1',
3632
+ 'fi',
3633
+ ];
3531
3634
  appendShWriteCommands(sh, botFiles(0));
3532
- sh.push('openclaw gateway run');
3635
+ sh.push('echo "Starting bot via Docker Compose..."');
3636
+ sh.push('if docker compose version > /dev/null 2>&1; then COMPOSE="docker compose"; else COMPOSE="docker-compose"; fi');
3637
+ sh.push('cd docker/openclaw');
3638
+ sh.push('$COMPOSE up --detach --build');
3639
+ sh.push('echo "\u2705 Bot dang chay via Docker. Xem log: docker logs -f openclaw-bot"');
3640
+ scriptContent = sh.filter(Boolean).join('\n');
3641
+ } else {
3642
+ // ── macOS Native mode: same approach as Ubuntu but no PM2, no apt ────────
3643
+ // Do NOT use 'npm config set prefix' on macOS — breaks Homebrew Node.
3644
+ // Use export npm_config_prefix per-session + sudo fallback.
3645
+ const sh = [
3646
+ '#!/usr/bin/env bash', 'set -e',
3647
+ 'echo "=== OpenClaw Setup \u2014 macOS Native ==="',
3648
+ 'command -v node > /dev/null 2>&1 || { echo "ERROR: Node.js chua cai! https://nodejs.org"; exit 1; }',
3649
+ '# User-local npm prefix (Homebrew-safe — no global npmrc mutation)',
3650
+ 'mkdir -p "$HOME/.local/bin"',
3651
+ 'export npm_config_prefix="$HOME/.local"',
3652
+ 'export PATH="$HOME/.local/bin:$PATH"',
3653
+ 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.zshrc"',
3654
+ 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3655
+ '# Install openclaw (user-local first, sudo fallback)',
3656
+ 'npm install -g openclaw@latest || sudo npm install -g openclaw@latest',
3657
+ ];
3658
+ providerLines(sh, 'sh');
3659
+ if (pluginCmd) sh.push(pluginCmd);
3660
+
3661
+ if (isMultiBot) {
3662
+ appendShWriteCommands(sh, sharedNativeFileMap());
3663
+ sh.push('echo "Starting shared multi-bot gateway..."');
3664
+ sh.push('openclaw gateway run');
3665
+ } else {
3666
+ appendShWriteCommands(sh, botFiles(0));
3667
+ sh.push('openclaw gateway run');
3668
+ }
3669
+ scriptContent = sh.filter(Boolean).join('\n');
3533
3670
  }
3534
- scriptContent = sh.filter(Boolean).join('\n');
3535
3671
 
3536
3672
  // ─── VPS/Ubuntu PM2 .SH ──────────────────────────────────────────────────
3537
3673
  } else if (state.nativeOs === 'vps') {
@@ -3556,12 +3692,12 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3556
3692
 
3557
3693
  if (isMultiBot) {
3558
3694
  vps.push('echo "--- Creating shared multi-agent runtime ---"');
3559
- appendShWriteCommands(vps, sharedNativeFileMap());
3560
- vps.push('echo "--- Starting shared gateway via PM2 ---"');
3561
- if (is9Router) {
3562
- vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
3563
- vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3564
- }
3695
+ appendShWriteCommands(vps, sharedNativeFileMap());
3696
+ vps.push('echo "--- Starting shared gateway via PM2 ---"');
3697
+ if (is9Router) {
3698
+ vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
3699
+ vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3700
+ }
3565
3701
  vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
3566
3702
  vps.push('pm2 save && pm2 startup');
3567
3703
  vps.push(`echo ""`);
@@ -3569,12 +3705,12 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3569
3705
  vps.push(`echo "Commands:"`);
3570
3706
  vps.push(`echo " pm2 status # Status gateway"`);
3571
3707
  vps.push(`echo " pm2 logs openclaw-multibot"`);
3572
- } else {
3573
- appendShWriteCommands(vps, botFiles(0));
3574
- if (is9Router) {
3575
- vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-9router --interpreter "$(command -v node)"');
3576
- vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3577
- }
3708
+ } else {
3709
+ appendShWriteCommands(vps, botFiles(0));
3710
+ if (is9Router) {
3711
+ vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-9router --interpreter "$(command -v node)"');
3712
+ vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3713
+ }
3578
3714
  vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
3579
3715
  vps.push('pm2 save && pm2 startup');
3580
3716
  vps.push('echo "Bot dang chay! Xem log: pm2 logs openclaw"');
@@ -3881,14 +4017,14 @@ echo ""
3881
4017
 
3882
4018
  script += `# \${isVi ? 'Tạo thư mục' : 'Create directories'}\n`;
3883
4019
  Array.from(dirs).sort().forEach(dir => {
3884
- script += `mkdir -p "\${dir}"\n`;
4020
+ script += `mkdir -p "${dir}"\n`;
3885
4021
  });
3886
4022
  script += '\n';
3887
4023
 
3888
4024
  Object.entries(files).forEach(([path, content]) => {
3889
4025
  script += `# \${path}\n`;
3890
4026
  const contentStr = typeof content === 'string' ? content : '';
3891
- script += `cat > "\${path}" << 'CLAWEOF'\n`;
4027
+ script += `cat > "${path}" << 'CLAWEOF'\n`;
3892
4028
  script += contentStr;
3893
4029
  if (!contentStr.endsWith('\n')) script += '\n';
3894
4030
  script += `CLAWEOF\n\n`;
@@ -3899,6 +4035,7 @@ echo ""
3899
4035
  script += `echo ""\n`;
3900
4036
  script += `echo "\${isVi ? '🐳 Đang khởi động Docker (có thể mất vài phút)...' : '🐳 Starting Docker (may take a few minutes)...'}"\n`;
3901
4037
  script += `if docker compose version > /dev/null 2>&1; then\n COMPOSE_CMD="docker compose"\nelif docker-compose version > /dev/null 2>&1; then\n COMPOSE_CMD="docker-compose"\nelse\n echo "\${isVi ? '❌ Không tìm thấy Docker Compose! Cài bằng: sudo apt-get install docker-compose-plugin' : '❌ Docker Compose not found! Install: sudo apt-get install docker-compose-plugin'}"\n exit 1\nfi\n`;
4038
+ script += `# Check Docker daemon is actually running\nif ! docker info > /dev/null 2>&1; then\n echo "${isVi ? '❌ Docker daemon chưa chạy! Hãy mở Docker Desktop rồi chạy lại.' : '❌ Docker daemon is not running! Open Docker Desktop first, then re-run this script.'}"; exit 1\nfi\n`;
3902
4039
 
3903
4040
  if (isMultiBot) {
3904
4041
  script += `cd "docker/openclaw"\n`;