create-openclaw-bot 5.1.13 → 5.1.15

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
@@ -161,14 +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
+ {
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: 'openclaw-telegram-multibot-relay',
170
+ hidden: true, // hidden in UI, auto-selected programmatically
171
+ },
172
172
  {
173
173
  id: 'voice-call',
174
174
  name: 'Voice Call',
@@ -804,8 +804,8 @@
804
804
  icon: '🐧',
805
805
  titleVi: 'Ubuntu / VPS — Khuyên dùng Native (Không Docker)',
806
806
  titleEn: 'Ubuntu / VPS — Recommended: Native (No Docker)',
807
- descVi: 'Chạy thẳng trên máy, tiết kiệm RAM, khởi động nhanh. Script tự cài Node.js 20 LTS, OpenClaw CLI, PM2, 9Router/Ollama và giữ bot chạy liên tục sau reboot.',
808
- descEn: 'Run directly on machine — lower RAM, faster startup. Script auto-installs Node.js 20 LTS, OpenClaw CLI, PM2, 9Router/Ollama and keeps bot running across reboots.',
807
+ descVi: 'Chạy thẳng trên máy, tiết kiệm RAM, khởi động nhanh. Script tự cài Node.js 20 LTS, OpenClaw CLI, PM2, 9Router/Ollama và giữ bot chạy liên tục sau reboot. Tạm tránh Node 25.',
808
+ descEn: 'Run directly on machine — lower RAM, faster startup. Script auto-installs Node.js 20 LTS, OpenClaw CLI, PM2, 9Router/Ollama and keeps bot running across reboots. Avoid Node 25 for now.',
809
809
  deploy: 'native',
810
810
  badgeVi: '💻 Native + PM2',
811
811
  badgeEn: '💻 Native + PM2',
@@ -815,8 +815,8 @@
815
815
  icon: '🖥️',
816
816
  titleVi: 'Linux Desktop — Khuyên dùng Native',
817
817
  titleEn: 'Linux Desktop — Recommended: Native',
818
- descVi: 'Không cần Docker. Script tự cài Node.js 20 LTS nếu chưa có, cài OpenClaw CLI, rồi cài 9Router hoặc Ollama theo provider bạn chọn và khởi động bot ngay.',
819
- descEn: 'No Docker needed. Script auto-installs Node.js 20 LTS if missing, installs OpenClaw CLI, then installs 9Router or Ollama based on your provider choice and starts the bot immediately.',
818
+ descVi: 'Không cần Docker. Script tự cài Node.js 20 LTS nếu chưa có, cài OpenClaw CLI, rồi cài 9Router hoặc Ollama theo provider bạn chọn và khởi động bot ngay. Tạm tránh Node 25.',
819
+ descEn: 'No Docker needed. Script auto-installs Node.js 20 LTS if missing, installs OpenClaw CLI, then installs 9Router or Ollama based on your provider choice and starts the bot immediately. Avoid Node 25 for now.',
820
820
  deploy: 'native',
821
821
  badgeVi: '💻 Native',
822
822
  badgeEn: '💻 Native',
@@ -1049,14 +1049,18 @@
1049
1049
  }).join('');
1050
1050
  }
1051
1051
 
1052
- window.__selectProvider = function (key) {
1053
- state.config.provider = key;
1054
- const p = PROVIDERS[key];
1055
- state.config.model = p.models[0].id;
1056
-
1057
- // Highlight card
1058
- document.querySelectorAll('.provider-card').forEach((c) => c.classList.remove('provider-card--selected'));
1059
- document.querySelector(`.provider-card[data-provider="${key}"]`)?.classList.add('provider-card--selected');
1052
+ window.__selectProvider = function (key) {
1053
+ state.config.provider = key;
1054
+ const p = PROVIDERS[key];
1055
+ state.config.model = p.models[0].id;
1056
+ if (state.bots[state.activeBotIndex]) {
1057
+ state.bots[state.activeBotIndex].provider = key;
1058
+ state.bots[state.activeBotIndex].model = p.models[0].id;
1059
+ }
1060
+
1061
+ // Highlight card
1062
+ document.querySelectorAll('.provider-card').forEach((c) => c.classList.remove('provider-card--selected'));
1063
+ document.querySelector(`.provider-card[data-provider="${key}"]`)?.classList.add('provider-card--selected');
1060
1064
 
1061
1065
  // Update model dropdown
1062
1066
  const modelSelect = document.getElementById('cfg-model');
@@ -1246,36 +1250,44 @@
1246
1250
  renderBotTabBar();
1247
1251
  }
1248
1252
 
1249
- function saveFormData() {
1250
- state.config.botName = document.getElementById('cfg-name')?.value || state.config.botName || 'Chat Bot';
1251
- state.config.description = document.getElementById('cfg-desc')?.value || state.config.description || 'Personal AI assistant';
1252
- state.config.emoji = document.getElementById('cfg-emoji')?.value || state.config.emoji || '🤖';
1253
- state.config.model = document.getElementById('cfg-model')?.value || state.config.model || 'google/gemini-2.5-flash';
1253
+ function saveFormData() {
1254
+ state.config.botName = document.getElementById('cfg-name')?.value || state.config.botName || 'Chat Bot';
1255
+ state.config.description = document.getElementById('cfg-desc')?.value || state.config.description || 'Personal AI assistant';
1256
+ state.config.emoji = document.getElementById('cfg-emoji')?.value || state.config.emoji || '🤖';
1257
+ state.config.model = document.getElementById('cfg-model')?.value || state.config.model || 'google/gemini-2.5-flash';
1254
1258
  state.config.language = document.getElementById('cfg-language')?.value || state.config.language || 'vi';
1255
1259
  state.config.systemPrompt = document.getElementById('cfg-prompt')?.value || state.config.systemPrompt || DEFAULT_PROMPTS['vi'];
1256
1260
  state.config.userInfo = document.getElementById('cfg-user-info')?.value?.trim() || state.config.userInfo || '';
1257
1261
  state.config.securityRules = document.getElementById('cfg-security')?.value || state.config.securityRules || DEFAULT_SECURITY_RULES['vi'];
1258
1262
  // 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
- }
1264
- }
1263
+ const tabName = document.getElementById('cfg-bot-tab-name')?.value?.trim();
1264
+ if (tabName && state.bots[0]) state.bots[0].name = tabName;
1265
+ else if (state.config.botName && state.bots[0] && !state.bots[0].name) {
1266
+ state.bots[0].name = state.config.botName;
1267
+ }
1268
+ if (state.bots[state.activeBotIndex]) {
1269
+ state.bots[state.activeBotIndex].provider = state.config.provider;
1270
+ state.bots[state.activeBotIndex].model = state.config.model;
1271
+ }
1272
+ }
1265
1273
 
1266
1274
  // Save Step 4 credential inputs to state (persists across Back navigation)
1267
- function saveCredentials() {
1268
- const botTokenEl = document.getElementById('key-bot-token');
1269
- const apiKeyEl = document.getElementById('key-api-key');
1270
- const pathEl = document.getElementById('cfg-project-path');
1271
- if (botTokenEl) state.config.botToken = botTokenEl.value;
1272
- if (apiKeyEl) state.config.apiKey = apiKeyEl.value;
1273
- if (pathEl) state.config.projectPath = pathEl.value;
1274
-
1275
- // Also save multi-bot tokens individually
1276
- if (state.botCount > 1) {
1277
- for (let i = 0; i < state.botCount; i++) {
1278
- const el = document.getElementById(`key-bot-token-${i}`);
1275
+ function saveCredentials() {
1276
+ const botTokenEl = document.getElementById('key-bot-token');
1277
+ const apiKeyEl = document.getElementById('key-api-key');
1278
+ const pathEl = document.getElementById('cfg-project-path');
1279
+ if (botTokenEl) state.config.botToken = botTokenEl.value;
1280
+ if (apiKeyEl) state.config.apiKey = apiKeyEl.value;
1281
+ if (pathEl) state.config.projectPath = pathEl.value;
1282
+ if (state.botCount <= 1 && state.bots[state.activeBotIndex]) {
1283
+ if (botTokenEl) state.bots[state.activeBotIndex].token = botTokenEl.value;
1284
+ if (apiKeyEl) state.bots[state.activeBotIndex].apiKey = apiKeyEl.value;
1285
+ }
1286
+
1287
+ // Also save multi-bot tokens individually
1288
+ if (state.botCount > 1) {
1289
+ for (let i = 0; i < state.botCount; i++) {
1290
+ const el = document.getElementById(`key-bot-token-${i}`);
1279
1291
  if (el && state.bots[i]) state.bots[i].token = el.value;
1280
1292
  }
1281
1293
  }
@@ -1534,7 +1546,7 @@
1534
1546
  const is9Router = provider.isProxy;
1535
1547
  const isLocal = provider.isLocal;
1536
1548
  const isTelegramMultiBot = state.botCount > 1 && state.channel === 'telegram';
1537
- const relayPluginSpec = 'clawhub:openclaw-telegram-multibot-relay';
1549
+ const relayPluginSpec = 'openclaw-telegram-multibot-relay';
1538
1550
 
1539
1551
  function buildRelayPluginInstallCommand(prefix) {
1540
1552
  return `${prefix} plugins install ${relayPluginSpec} 2>/dev/null || true`;
@@ -1545,7 +1557,8 @@
1545
1557
  }
1546
1558
 
1547
1559
  function buildTelegramPostInstallChecklist() {
1548
- const groupId = state.groupId || '';
1560
+ const groupId = state.groupId || '';
1561
+ const nativeProjectOpenClawRoot = `${projectDir.replace(/\\/g, '/')}/.openclaw`;
1549
1562
  const botList = state.bots.slice(0, state.botCount).map((bot, idx) => `- **${bot?.name || `Bot ${idx + 1}`}**`).join('\n');
1550
1563
  const isVi = lang === 'vi';
1551
1564
  return isVi
@@ -1688,14 +1701,15 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1688
1701
  commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
1689
1702
  channels: ch.channelConfig,
1690
1703
  tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
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, '') },
1704
+ gateway: {
1705
+ port: 18791,
1706
+ mode: 'local',
1707
+ bind: 'custom',
1708
+ customBindHost: '0.0.0.0',
1709
+ controlUi: {
1710
+ allowedOrigins: getGatewayAllowedOrigins(18791),
1711
+ },
1712
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
1699
1713
  },
1700
1714
  };
1701
1715
 
@@ -1793,13 +1807,14 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1793
1807
  botToken: meta.token || '<your_bot_token>',
1794
1808
  ackReaction: '👍',
1795
1809
  }]));
1796
- clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
1797
- id: meta.agentId,
1798
- name: meta.name,
1799
- workspace: `/root/.openclaw/${meta.workspaceDir}`,
1800
- agentDir: `/root/.openclaw/agents/${meta.agentId}/agent`,
1801
- model: { primary: state.config.model, fallbacks: [] },
1802
- }));
1810
+ const nativeOpenClawRoot = '.openclaw';
1811
+ clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
1812
+ id: meta.agentId,
1813
+ name: meta.name,
1814
+ workspace: `${nativeOpenClawRoot}/${meta.workspaceDir}`,
1815
+ agentDir: `${nativeOpenClawRoot}/agents/${meta.agentId}/agent`,
1816
+ model: { primary: state.config.model, fallbacks: [] },
1817
+ }));
1803
1818
  clawConfig.bindings = multiBotAgentMetas.map((meta) => ({
1804
1819
  agentId: meta.agentId,
1805
1820
  match: { channel: 'telegram', accountId: meta.accountId },
@@ -1940,7 +1955,7 @@ model:
1940
1955
  ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
1941
1956
  : '';
1942
1957
  // Patch config on every startup to keep gateway settings stable
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));}\\" && `;
1958
+ 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).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
1944
1959
  // Auto-approve device pairing after gateway starts (required since v2026.3.x)
1945
1960
  const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
1946
1961
  const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${autoApproveCmd}${gatewayCmd}"`;
@@ -1951,7 +1966,7 @@ RUN apt-get update && apt-get install -y git curl${browserAptExtra} && rm -rf /v
1951
1966
 
1952
1967
 
1953
1968
  ARG CACHEBUST=${Date.now()}
1954
- RUN npm install -g openclaw@latest${skillLines}${browserInstallLines}
1969
+ RUN npm install -g openclaw@2026.4.5 grammy${skillLines}${browserInstallLines}
1955
1970
  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);}"
1956
1971
  WORKDIR /root/.openclaw
1957
1972
 
@@ -1969,8 +1984,8 @@ ${finalCmd}`;
1969
1984
 
1970
1985
  // ─── Dynamic Smart Route Sync Script ────────────────────────────────────────
1971
1986
  // Background loop inside 9Router container every 30s.
1972
- // Read providerConnections directly from db.json so smart-route survives
1973
- // dashboard auth/response changes in newer 9Router builds.
1987
+ // Sync against the 9Router API so smart-route matches the current
1988
+ // active provider set instead of stale db-only state.
1974
1989
  const syncScript = `const fs=require('fs');const INTERVAL=30000;const p='/root/.9router/db.json';
1975
1990
  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']};
1976
1991
  console.log('[sync-combo] 9Router sync loop started...');
@@ -2759,13 +2774,14 @@ fi
2759
2774
  id: botAgentId,
2760
2775
  model: { primary: state.config.model, fallbacks: [] },
2761
2776
  }];
2762
- botConfig.gateway = {
2763
- ...(botConfig.gateway || {}),
2764
- port: 18791,
2765
- mode: 'local',
2766
- bind: '0.0.0.0',
2767
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
2768
- };
2777
+ botConfig.gateway = {
2778
+ ...(botConfig.gateway || {}),
2779
+ port: 18791,
2780
+ mode: 'local',
2781
+ bind: 'custom',
2782
+ customBindHost: '0.0.0.0',
2783
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
2784
+ };
2769
2785
 
2770
2786
  const botAgentYaml = `name: ${botAgentId}
2771
2787
  description: "${botDesc}"
@@ -2967,41 +2983,62 @@ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
2967
2983
  const ch = CHANNELS[state.channel];
2968
2984
  const is9Router = !!(provider && provider.isProxy);
2969
2985
  const isOllama = !!(provider && provider.isLocal);
2970
- const hasBrowser = state.config.skills.includes('browser');
2971
- const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
2986
+ const hasBrowser = state.config.skills.includes('browser');
2987
+ const nativeSkillConfigs = state.config.skills
2988
+ .map((sid) => SKILLS.find((s) => s.id === sid))
2989
+ .filter((skill) => skill && skill.id !== 'scheduler' && skill.slug && skill.slug !== 'browser-automation');
2990
+ const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
2972
2991
  const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
2973
2992
  const projectDir = state.config.projectPath || '.';
2974
2993
 
2975
- const allPlugins = [];
2994
+ const allPlugins = [];
2976
2995
  if (ch && ch.pluginInstall) allPlugins.push(ch.pluginInstall);
2977
2996
  state.config.plugins.forEach(function(pid) {
2978
2997
  const p = PLUGINS.find((x) => x.id === pid);
2979
2998
  if (p) allPlugins.push(p.package);
2980
2999
  });
2981
- if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
2982
- const pluginCmd = allPlugins.length > 0 ? ('npm exec openclaw plugins install ' + allPlugins.join(' ')) : '';
2983
-
2984
- function native9RouterSyncScriptContent() {
2985
- return `const fs=require('fs');
2986
- const path=require('path');
2987
- const INTERVAL=30000;
2988
- const p=path.join(process.env.HOME||process.env.USERPROFILE||'.','.9router','db.json');
2989
- 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']};
2990
- const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catch{}if(!db.combos)db.combos=[];const removeSmartRoute=()=>{const next=db.combos.filter(x=>x.id!=='smart-route');if(next.length!==db.combos.length){db.combos=next;fs.writeFileSync(p,JSON.stringify(db,null,2));}};const a=(db.providerConnections||[]).filter(c=>c&&c.provider&&c.isActive!==false&&!c.disabled).map(c=>c.provider);if(!a.length){removeSmartRoute();return;}const PREF=['openai','anthropic','claude-code','codex','cursor','github','cline','kimi','minimax','deepseek','glm','alicode','xai','mistral','kilo','kiro','iflow','qwen','gemini-cli','ollama'];a.sort((x,y)=>(PREF.indexOf(x)===-1?99:PREF.indexOf(x))-(PREF.indexOf(y)===-1?99:PREF.indexOf(y)));const m=a.flatMap(provider=>PM[provider]||[]);if(!m.length){removeSmartRoute();return;}const c={id:'smart-route',name:'smart-route',alias:'smart-route',models:m};const i=db.combos.findIndex(x=>x.id==='smart-route');if(i>=0){if(JSON.stringify(db.combos[i].models)!==JSON.stringify(c.models)){db.combos[i]=c;fs.writeFileSync(p,JSON.stringify(db,null,2));}}else{db.combos.push(c);fs.writeFileSync(p,JSON.stringify(db,null,2));}}catch{}};sync();setInterval(sync,INTERVAL);`;
2991
- }
2992
-
2993
- // ─── Shared initializer (provider install) ───────────────────────────────
2994
- function providerLines(arr, shell) {
2995
- if (is9Router) {
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 &');
3000
+ if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
3001
+ const pluginCmd = allPlugins.length > 0 ? ('call npm exec -- openclaw plugins install ' + allPlugins.join(' ') + ' || goto :fail') : '';
3002
+ const nativeSkillInstallCmds = nativeSkillConfigs.map((skill) => `call openclaw skills install ${skill.slug} || echo Warning: Failed to install skill ${skill.slug}`);
3003
+
3004
+ function native9RouterSyncScriptContent() {
3005
+ return `const fs=require('fs');
3006
+ const path=require('path');
3007
+ const INTERVAL=30000;
3008
+ const p=path.join(process.env.DATA_DIR||'.9router','db.json');
3009
+ const ROUTER='http://localhost:20128';
3010
+ 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']};
3011
+ console.log('[sync-combo] 9Router sync loop started...');
3012
+ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.ok){console.log('[sync-combo] API not ready, retrying...');return;}const d=await res.json();const a=(d.connections||[]).filter(c=>c&&c.provider&&c.isActive!==false&&!c.disabled).map(c=>c.provider);let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catch{}if(!db.combos)db.combos=[];const removeSmartRoute=()=>{const next=db.combos.filter(x=>x.id!=='smart-route');if(next.length!==db.combos.length){db.combos=next;fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Removed smart-route (no active providers)');}};if(!a.length){removeSmartRoute();return;}const PREF=['openai','anthropic','claude-code','codex','cursor','github','cline','kimi','minimax','deepseek','glm','alicode','xai','mistral','kilo','kiro','iflow','qwen','gemini-cli','ollama'];a.sort((x,y)=>(PREF.indexOf(x)===-1?99:PREF.indexOf(x))-(PREF.indexOf(y)===-1?99:PREF.indexOf(y)));const m=a.flatMap(provider=>PM[provider]||[]);if(!m.length){removeSmartRoute();return;}const c={id:'smart-route',name:'smart-route',alias:'smart-route',models:m};const i=db.combos.findIndex(x=>x.id==='smart-route');if(i>=0){if(JSON.stringify(db.combos[i].models)!==JSON.stringify(c.models)){db.combos[i]=c;fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Updated smart-route: '+c.models.length+' models from: '+a.join(','));}}else{db.combos.push(c);fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Created smart-route: '+c.models.length+' models from: '+a.join(','));}}catch(e){console.log('[sync-combo] Error:',e.message);}};setTimeout(sync,5000);setInterval(sync,INTERVAL);`;
3013
+ }
3014
+
3015
+ function native9RouterServerEntryLookup() {
3016
+ return "node -e \"const fs=require('fs'),path=require('path'),os=require('os'),cp=require('child_process');const home=os.homedir();const roots=[];try{const root=cp.execSync('npm root -g',{stdio:['ignore','pipe','ignore'],encoding:'utf8'}).trim();if(root)roots.push(root);}catch{}for(const prefix of [process.env.npm_config_prefix,process.env.NPM_CONFIG_PREFIX,process.env.PREFIX,process.env.NPM_PREFIX,path.join(home,'.local'),path.join(home,'.npm-global'),path.join(home,'.local','share','npm')].filter(Boolean)){roots.push(path.join(prefix,'lib','node_modules'));}roots.push(path.join(home,'.local','share','npm','lib','node_modules'));roots.push(path.join(home,'.local','lib','node_modules'));const seen=new Set();const found=roots.map(root=>path.join(root,'9router','app','server.js')).find(candidate=>{if(seen.has(candidate))return false;seen.add(candidate);return fs.existsSync(candidate);});if(!found)process.exit(1);console.log(found);\"";
3017
+ }
3018
+
3019
+ function windowsHiddenNodeLaunch(targetPath, extraEnv = {}) {
3020
+ function quotePowerShellSingle(value) {
3021
+ return `'${String(value).replace(/'/g, "''")}'`;
3022
+ }
3023
+ const envAssignments = Object.entries(extraEnv)
3024
+ .map(([key, value]) => `$env:${key}=${quotePowerShellSingle(String(value))}`)
3025
+ .join('; ');
3026
+ return `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${envAssignments ? `${envAssignments}; ` : ''}Start-Process -WindowStyle Hidden -FilePath (Get-Command node).Source -ArgumentList @('${targetPath.replace(/'/g, "''")}')"`;
3027
+ }
3028
+
3029
+ // ─── Shared initializer (provider install) ───────────────────────────────
3030
+ function providerLines(arr, shell) {
3031
+ if (is9Router) {
3032
+ if (shell === 'bat') {
3033
+ arr.push('call npm install -g 9router || goto :fail');
3034
+ arr.push(`for /f "usebackq delims=" %%I in (\`${native9RouterServerEntryLookup()}\`) do set "NINE_ROUTER_ENTRY=%%I"`);
3035
+ arr.push(windowsHiddenNodeLaunch('%NINE_ROUTER_ENTRY%', { PORT: '20128', HOSTNAME: '0.0.0.0', DATA_DIR: '%DATA_DIR%' }));
3036
+ arr.push('timeout /t 5 /nobreak >nul');
3037
+ } else {
3038
+ arr.push('npm install -g 9router');
3039
+ arr.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
3040
+ arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 DATA_DIR="$PWD/.9router" node "$NINE_ROUTER_ENTRY" >/tmp/9router.log 2>&1 &');
3041
+ arr.push('nohup env DATA_DIR="$PWD/.9router" node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
3005
3042
  arr.push('sleep 3');
3006
3043
  }
3007
3044
  } else if (isOllama) {
@@ -3062,7 +3099,7 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3062
3099
  order: { ollama: ['ollama:default'] },
3063
3100
  };
3064
3101
  } else {
3065
- const authProviderName = provider.isProxy ? '9router' : provider.id;
3102
+ const authProviderName = provider.isProxy ? '9router' : state.config.provider;
3066
3103
  const authProfileId = provider.isProxy ? '9router-proxy' : `${authProviderName}:default`;
3067
3104
  const authKeyValue = provider.isProxy
3068
3105
  ? 'sk-no-key'
@@ -3118,8 +3155,8 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3118
3155
  list: multiBotAgentMetas.map((meta) => ({
3119
3156
  id: meta.agentId,
3120
3157
  name: meta.name,
3121
- workspace: `./.openclaw/${meta.workspaceDir}`,
3122
- agentDir: `./.openclaw/agents/${meta.agentId}/agent`,
3158
+ workspace: `${nativeProjectOpenClawRoot}/${meta.workspaceDir}`,
3159
+ agentDir: `${nativeProjectOpenClawRoot}/agents/${meta.agentId}/agent`,
3123
3160
  model: { primary: state.config.model, fallbacks: [] },
3124
3161
  })),
3125
3162
  },
@@ -3161,14 +3198,15 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3161
3198
  'telegram-multibot-relay': { enabled: true },
3162
3199
  },
3163
3200
  },
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, '') },
3201
+ gateway: {
3202
+ port: 18791,
3203
+ mode: 'local',
3204
+ bind: 'custom',
3205
+ customBindHost: '0.0.0.0',
3206
+ controlUi: {
3207
+ allowedOrigins: getGatewayAllowedOrigins(18791),
3208
+ },
3209
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3172
3210
  },
3173
3211
  };
3174
3212
  return JSON.stringify(cfg, null, 2);
@@ -3254,44 +3292,116 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3254
3292
  }
3255
3293
 
3256
3294
  // ─── Per-bot openclaw.json (minimal — shared workspace) ──────────────────
3257
- function botConfigContent(botIndex) {
3258
- const bot = state.bots[botIndex] || {};
3259
- const botName = bot.name || `Bot ${botIndex + 1}`;
3260
- const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3261
- const basePort = 18791 + botIndex;
3262
- const groupId = state.groupId || '';
3263
- const channelConfig = JSON.parse(JSON.stringify(ch.channelConfig || {}));
3264
- if (state.channel === 'telegram' && isMultiBot) {
3265
- channelConfig.groupPolicy = groupId ? 'allowlist' : 'open';
3266
- channelConfig.groupAllowFrom = ['*'];
3267
- channelConfig.groups = {
3268
- [groupId || '*']: {
3269
- enabled: true,
3270
- requireMention: false,
3271
- },
3272
- };
3273
- }
3274
- const cfg = {
3275
- meta: { lastTouchedVersion: '2026.3.24' },
3276
- agents: {
3277
- defaults: { model: { primary: bot.model || state.config.model }, compaction: { mode: 'safeguard' }, timeoutSeconds: 120 },
3278
- list: [{ id: agentId, model: { primary: bot.model || state.config.model } }],
3279
- },
3280
- commands: { native: 'auto', nativeSkills: 'auto', restart: true },
3281
- channels: channelConfig,
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
- },
3291
-
3292
- };
3293
- return JSON.stringify(cfg, null, 2);
3294
- }
3295
+ function botConfigContent(botIndex) {
3296
+ const bot = state.bots[botIndex] || {};
3297
+ const botName = bot.name || `Bot ${botIndex + 1}`;
3298
+ const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3299
+ const basePort = 18791 + botIndex;
3300
+ const groupId = state.groupId || '';
3301
+ const botProvider = PROVIDERS[bot.provider] || provider;
3302
+ const cfg = {
3303
+ meta: { lastTouchedVersion: '2026.3.24' },
3304
+ agents: {
3305
+ defaults: {
3306
+ model: { primary: bot.model || state.config.model },
3307
+ compaction: { mode: 'safeguard' },
3308
+ timeoutSeconds: botProvider.isLocal ? 900 : 120,
3309
+ ...(botProvider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
3310
+ },
3311
+ list: [{ id: agentId, model: { primary: bot.model || state.config.model } }],
3312
+ },
3313
+ ...(botProvider.isProxy ? {
3314
+ models: {
3315
+ mode: 'merge',
3316
+ providers: {
3317
+ '9router': {
3318
+ baseUrl: 'http://localhost:20128/v1',
3319
+ apiKey: 'sk-no-key',
3320
+ api: 'openai-completions',
3321
+ models: [
3322
+ {
3323
+ id: 'smart-route',
3324
+ name: 'Smart Proxy (Auto Route)',
3325
+ contextWindow: 200000,
3326
+ maxTokens: 8192,
3327
+ }
3328
+ ]
3329
+ }
3330
+ }
3331
+ }
3332
+ } : {}),
3333
+ ...(botProvider.isLocal ? {
3334
+ models: {
3335
+ providers: {
3336
+ ollama: {
3337
+ baseUrl: 'http://localhost:11434',
3338
+ apiKey: 'ollama-local',
3339
+ api: 'ollama',
3340
+ models: [
3341
+ { id: selectedModel, name: selectedModel, contextWindow: 128000, maxTokens: 8192 }
3342
+ ]
3343
+ }
3344
+ }
3345
+ }
3346
+ } : {}),
3347
+ commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
3348
+ channels: {},
3349
+ tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
3350
+ gateway: {
3351
+ port: basePort,
3352
+ mode: 'local',
3353
+ bind: 'custom',
3354
+ customBindHost: '0.0.0.0',
3355
+ controlUi: {
3356
+ allowedOrigins: getGatewayAllowedOrigins(basePort),
3357
+ },
3358
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3359
+ },
3360
+ };
3361
+
3362
+ if (hasBrowser) {
3363
+ cfg.browser = { enabled: true };
3364
+ }
3365
+
3366
+ const skillEntries = {};
3367
+ state.config.skills.forEach((sid) => {
3368
+ const skill = SKILLS.find((s) => s.id === sid);
3369
+ if (!skill) return;
3370
+ if (skill.id === 'scheduler' || skill.slug === 'browser-automation' || !skill.slug) return;
3371
+ skillEntries[skill.slug] = { enabled: true };
3372
+ });
3373
+ if (Object.keys(skillEntries).length > 0) {
3374
+ cfg.skills = { entries: skillEntries };
3375
+ }
3376
+
3377
+ if (state.channel === 'telegram') {
3378
+ cfg.channels.telegram = {
3379
+ enabled: true,
3380
+ dmPolicy: 'open',
3381
+ allowFrom: ['*'],
3382
+ };
3383
+ if (isMultiBot) {
3384
+ cfg.channels.telegram.groupPolicy = groupId ? 'allowlist' : 'open';
3385
+ cfg.channels.telegram.groupAllowFrom = ['*'];
3386
+ cfg.channels.telegram.groups = {
3387
+ [groupId || '*']: {
3388
+ enabled: true,
3389
+ requireMention: false,
3390
+ },
3391
+ };
3392
+ }
3393
+ } else if (state.channel === 'zalo-personal') {
3394
+ cfg.channels.zalouser = {
3395
+ enabled: true,
3396
+ dmPolicy: 'open',
3397
+ autoReply: true,
3398
+ };
3399
+ } else if (state.channel === 'zalo-bot') {
3400
+ cfg.channels.zalo = { enabled: true, provider: 'official_account' };
3401
+ }
3402
+
3403
+ return JSON.stringify(cfg, null, 2);
3404
+ }
3295
3405
 
3296
3406
  function botAuthProfilesContent(botIndex) {
3297
3407
  const bot = state.bots[botIndex] || {};
@@ -3311,7 +3421,7 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
3311
3421
  order: { ollama: ['ollama:default'] },
3312
3422
  };
3313
3423
  } else {
3314
- const authProviderName = botProvider.isProxy ? '9router' : botProvider.id;
3424
+ const authProviderName = botProvider.isProxy ? '9router' : (bot.provider || state.config.provider);
3315
3425
  const authProfileId = botProvider.isProxy ? '9router-proxy' : `${authProviderName}:default`;
3316
3426
  const authKeyValue = botProvider.isProxy
3317
3427
  ? 'sk-no-key'
@@ -3567,18 +3677,29 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3567
3677
  .replace(/%/g, '%%');
3568
3678
  }
3569
3679
 
3570
- function appendBatWriteCommands(arr, files) {
3571
- Object.entries(files).forEach(([relPath, content]) => {
3572
- const winPath = relPath.replace(/\//g, '\\');
3573
- const dir = winPath.substring(0, winPath.lastIndexOf('\\'));
3574
- if (dir) arr.push(`if not exist "${dir}" mkdir "${dir}"`);
3680
+ function appendBatWriteCommands(arr, files) {
3681
+ Object.entries(files).forEach(([relPath, content]) => {
3682
+ const winPath = relPath.replace(/\//g, '\\');
3683
+ const dir = winPath.substring(0, winPath.lastIndexOf('\\'));
3684
+ if (dir) arr.push(`if not exist "${dir}" mkdir "${dir}"`);
3575
3685
  arr.push(`> "${winPath}" (`);
3576
3686
  content.split('\n').forEach((line) => {
3577
3687
  arr.push(line.length ? `echo(${batEscapeEchoLine(line)}` : 'echo(');
3578
3688
  });
3579
- arr.push(')');
3580
- });
3581
- }
3689
+ arr.push(')');
3690
+ });
3691
+ }
3692
+
3693
+ function mapWindowsNativeFiles(files) {
3694
+ return Object.fromEntries(Object.entries(files).map(([relPath, content]) => {
3695
+ const normalized = relPath.replace(/\\/g, '/');
3696
+ if (normalized === '.env') return ['%PROJECT_DIR%\\.env', content];
3697
+ if (normalized.startsWith('.openclaw/')) {
3698
+ return [`%OPENCLAW_HOME%\\${normalized.slice('.openclaw/'.length).replace(/\//g, '\\')}`, content];
3699
+ }
3700
+ return [`%PROJECT_DIR%\\${normalized.replace(/\//g, '\\')}`, content];
3701
+ }));
3702
+ }
3582
3703
 
3583
3704
  let scriptContent = '';
3584
3705
  let scriptName = '';
@@ -3587,33 +3708,82 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3587
3708
  if (state.nativeOs === 'win') {
3588
3709
  const isDocker = state.deployMode === 'docker';
3589
3710
  scriptName = isDocker ? 'setup-openclaw-docker-win.bat' : 'setup-openclaw-win.bat';
3590
- const lines = [
3591
- '@echo off',
3592
- 'chcp 65001 >nul',
3593
- `echo === OpenClaw Setup — Windows${isDocker ? ' Docker' : ' Native'} ===`,
3594
- 'echo.',
3595
- 'echo [1/5] Kiem tra Node.js...',
3711
+ const lines = [
3712
+ '@echo off',
3713
+ 'setlocal EnableExtensions',
3714
+ 'chcp 65001 >nul',
3715
+ `set "PROJECT_DIR=${projectDir.replace(/\//g, '\\')}"`,
3716
+ 'if not exist "%PROJECT_DIR%" mkdir "%PROJECT_DIR%"',
3717
+ 'cd /d "%PROJECT_DIR%"',
3718
+ 'set "OPENCLAW_HOME=%PROJECT_DIR%\\.openclaw"',
3719
+ 'set "OPENCLAW_STATE_DIR=%PROJECT_DIR%\\.openclaw"',
3720
+ 'set "DATA_DIR=%PROJECT_DIR%\\.9router"',
3721
+ 'set "PATH=%APPDATA%\\npm;%PATH%"',
3722
+ `echo === OpenClaw Setup — Windows${isDocker ? ' Docker' : ' Native'} ===`,
3723
+ 'echo.',
3724
+ 'echo [1/5] Kiem tra Node.js...',
3596
3725
  'where node >nul 2>&1 || (echo ERROR: Node.js chua cai! Tai tai: https://nodejs.org && pause && exit /b 1)',
3597
3726
  'echo [2/5] Cai OpenClaw CLI...',
3598
- 'npm install -g openclaw@latest',
3599
- ];
3600
- providerLines(lines, 'bat');
3601
- if (pluginCmd) { lines.push('echo Cai plugins...'); lines.push(pluginCmd); }
3602
-
3603
- if (isMultiBot) {
3604
- lines.push('echo [4/5] Tao runtime multi-agent dung chung...');
3605
- appendBatWriteCommands(lines, sharedNativeFileMap());
3606
- lines.push('echo [5/5] Khoi dong gateway multi-bot...');
3607
- lines.push('openclaw gateway run');
3608
- } else {
3609
- lines.push('echo [4/5] Tao file cau hinh...');
3610
- appendBatWriteCommands(lines, botFiles(0));
3611
- lines.push('echo [5/5] Khoi dong bot...');
3612
- lines.push('openclaw gateway run');
3613
- }
3614
-
3615
- lines.push('pause');
3616
- scriptContent = lines.filter(Boolean).join('\r\n');
3727
+ 'call npm install -g openclaw@2026.4.5 || goto :fail',
3728
+ ];
3729
+ providerLines(lines, 'bat');
3730
+ if (hasBrowser) {
3731
+ lines.push('echo Cai Browser Automation runtime...');
3732
+ lines.push('call npm install -g agent-browser playwright || goto :fail');
3733
+ lines.push('call npx playwright install chromium || goto :fail');
3734
+ }
3735
+ if (nativeSkillInstallCmds.length > 0) {
3736
+ lines.push('echo Cai skills...');
3737
+ lines.push(...nativeSkillInstallCmds);
3738
+ }
3739
+ if (pluginCmd) { lines.push('echo Cai plugins...'); lines.push(pluginCmd); }
3740
+ lines.push('if not exist "%OPENCLAW_HOME%" mkdir "%OPENCLAW_HOME%"');
3741
+ lines.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
3742
+
3743
+ if (isMultiBot) {
3744
+ lines.push('echo [4/5] Tao runtime multi-agent dung chung...');
3745
+ appendBatWriteCommands(lines, mapWindowsNativeFiles(sharedNativeFileMap()));
3746
+ if (is9Router) lines.push(windowsHiddenNodeLaunch('%OPENCLAW_HOME%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
3747
+ lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
3748
+ lines.push('echo.');
3749
+ lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
3750
+ lines.push('echo Other reachable URLs: http://localhost:18791');
3751
+ lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
3752
+ if (is9Router) {
3753
+ lines.push('echo.');
3754
+ lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
3755
+ lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
3756
+ }
3757
+ lines.push('echo [5/5] Khoi dong gateway multi-bot...');
3758
+ lines.push('call openclaw gateway run');
3759
+ } else {
3760
+ lines.push('echo [4/5] Tao file cau hinh...');
3761
+ appendBatWriteCommands(lines, mapWindowsNativeFiles(botFiles(0)));
3762
+ if (is9Router) lines.push(windowsHiddenNodeLaunch('%OPENCLAW_HOME%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
3763
+ lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
3764
+ lines.push('echo.');
3765
+ lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
3766
+ lines.push('echo Other reachable URLs: http://localhost:18791');
3767
+ lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
3768
+ if (is9Router) {
3769
+ lines.push('echo.');
3770
+ lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
3771
+ lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
3772
+ }
3773
+ lines.push('echo [5/5] Khoi dong bot...');
3774
+ lines.push('call openclaw gateway run');
3775
+ }
3776
+
3777
+ lines.push('goto :end');
3778
+ lines.push(':fail');
3779
+ lines.push('echo.');
3780
+ lines.push('echo Cai dat that bai. Kiem tra dong loi ngay phia tren.');
3781
+ lines.push('pause');
3782
+ lines.push('exit /b 1');
3783
+ lines.push(':end');
3784
+ lines.push('pause');
3785
+ lines.push('endlocal');
3786
+ scriptContent = lines.filter(Boolean).join('\r\n');
3617
3787
 
3618
3788
  // ─── macOS .SH ───────────────────────────────────────────────────────────
3619
3789
  } else if (state.nativeOs === 'linux') {
@@ -3642,18 +3812,24 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3642
3812
  // ── macOS Native mode: same approach as Ubuntu but no PM2, no apt ────────
3643
3813
  // Do NOT use 'npm config set prefix' on macOS — breaks Homebrew Node.
3644
3814
  // 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; }',
3815
+ const sh = [
3816
+ '#!/usr/bin/env bash', 'set -e',
3817
+ 'echo "=== OpenClaw Setup \u2014 macOS Native ==="',
3818
+ 'command -v node > /dev/null 2>&1 || { echo "ERROR: Node.js chua cai! https://nodejs.org"; exit 1; }',
3649
3819
  '# 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',
3820
+ 'mkdir -p "$HOME/.local/bin"',
3821
+ 'export npm_config_prefix="$HOME/.local"',
3822
+ 'export PATH="$HOME/.local/bin:$PATH"',
3823
+ `PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
3824
+ 'mkdir -p "$PROJECT_DIR"',
3825
+ 'cd "$PROJECT_DIR"',
3826
+ 'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
3827
+ 'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
3828
+ 'export DATA_DIR="$PROJECT_DIR/.9router"',
3829
+ 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.zshrc"',
3830
+ 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3831
+ '# Install openclaw (user-local first, sudo fallback)',
3832
+ 'npm install -g openclaw@2026.4.5 || sudo npm install -g openclaw@2026.4.5',
3657
3833
  ];
3658
3834
  providerLines(sh, 'sh');
3659
3835
  if (pluginCmd) sh.push(pluginCmd);
@@ -3672,20 +3848,26 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3672
3848
  // ─── VPS/Ubuntu PM2 .SH ──────────────────────────────────────────────────
3673
3849
  } else if (state.nativeOs === 'vps') {
3674
3850
  scriptName = 'setup-openclaw-vps.sh';
3675
- const vps = [
3676
- '#!/usr/bin/env bash', 'set -e',
3851
+ const vps = [
3852
+ '#!/usr/bin/env bash', 'set -e',
3677
3853
  `echo "=== OpenClaw Setup — Ubuntu/VPS${isMultiBot ? ` Multi-Bot (${state.botCount} bots)` : ''} ==="`,
3678
3854
  '# Auto-install Node.js 20 LTS if missing',
3679
3855
  'if ! command -v node > /dev/null 2>&1; then',
3680
3856
  ' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
3681
3857
  ' sudo apt-get install -y nodejs',
3682
3858
  'fi',
3683
- 'mkdir -p "$HOME/.local/bin"',
3684
- 'npm config set prefix "$HOME/.local"',
3685
- 'export PATH="$HOME/.local/bin:$PATH"',
3686
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
3687
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3688
- 'npm install -g openclaw@latest pm2@latest',
3859
+ 'mkdir -p "$HOME/.local/bin"',
3860
+ 'npm config set prefix "$HOME/.local"',
3861
+ 'export PATH="$HOME/.local/bin:$PATH"',
3862
+ `PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
3863
+ 'mkdir -p "$PROJECT_DIR"',
3864
+ 'cd "$PROJECT_DIR"',
3865
+ 'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
3866
+ 'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
3867
+ 'export DATA_DIR="$PROJECT_DIR/.9router"',
3868
+ 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
3869
+ 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3870
+ 'npm install -g openclaw@2026.4.5 pm2@latest',
3689
3871
  ];
3690
3872
  providerLines(vps, 'sh');
3691
3873
  if (pluginCmd) vps.push(pluginCmd);
@@ -3695,7 +3877,8 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3695
3877
  appendShWriteCommands(vps, sharedNativeFileMap());
3696
3878
  vps.push('echo "--- Starting shared gateway via PM2 ---"');
3697
3879
  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)"');
3880
+ vps.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
3881
+ vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$NINE_ROUTER_ENTRY" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
3699
3882
  vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3700
3883
  }
3701
3884
  vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
@@ -3708,7 +3891,8 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3708
3891
  } else {
3709
3892
  appendShWriteCommands(vps, botFiles(0));
3710
3893
  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)"');
3894
+ vps.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
3895
+ vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$NINE_ROUTER_ENTRY" --name openclaw-9router --interpreter "$(command -v node)"');
3712
3896
  vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
3713
3897
  }
3714
3898
  vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
@@ -3727,12 +3911,18 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3727
3911
  ' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
3728
3912
  ' sudo apt-get install -y nodejs',
3729
3913
  'fi',
3730
- 'mkdir -p "$HOME/.local/bin"',
3731
- 'npm config set prefix "$HOME/.local"',
3732
- 'export PATH="$HOME/.local/bin:$PATH"',
3733
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
3734
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3735
- 'npm install -g openclaw@latest',
3914
+ 'mkdir -p "$HOME/.local/bin"',
3915
+ 'npm config set prefix "$HOME/.local"',
3916
+ 'export PATH="$HOME/.local/bin:$PATH"',
3917
+ `PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
3918
+ 'mkdir -p "$PROJECT_DIR"',
3919
+ 'cd "$PROJECT_DIR"',
3920
+ 'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
3921
+ 'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
3922
+ 'export DATA_DIR="$PROJECT_DIR/.9router"',
3923
+ 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
3924
+ 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3925
+ 'npm install -g openclaw@2026.4.5',
3736
3926
  ];
3737
3927
  providerLines(lnx, 'sh');
3738
3928
  if (pluginCmd) lnx.push(pluginCmd);
@@ -3766,7 +3956,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3766
3956
  if (stepsList) {
3767
3957
  const steps = [];
3768
3958
  steps.push(isVi ? '✅ Kiểm tra Node.js (cài tự động trên Ubuntu/VPS nếu chưa có)' : '✅ Check Node.js (auto-install on Ubuntu/VPS if missing)');
3769
- steps.push(isVi ? '📦 Cài OpenClaw CLI (<code>npm install -g openclaw@latest</code>)' : '📦 Install OpenClaw CLI (<code>npm install -g openclaw@latest</code>)');
3959
+ steps.push(isVi ? '📦 Cài OpenClaw CLI (<code>npm install -g openclaw@2026.4.5</code>)' : '📦 Install OpenClaw CLI (<code>npm install -g openclaw@2026.4.5</code>)');
3770
3960
  if (is9Router) {
3771
3961
  steps.push(isVi ? '🔀 Cài 9Router (<code>npm install -g 9router</code>) và khởi động tự động' : '🔀 Install 9Router (<code>npm install -g 9router</code>) and start automatically');
3772
3962
  } else if (isOllama) {
@@ -3791,10 +3981,12 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3791
3981
 
3792
3982
 
3793
3983
 
3794
- window.downloadNativeScript = function() {
3795
- const script = window._nativeScript;
3796
- if (!script) return;
3797
- const blob = new Blob([script.content], { type: 'text/plain;charset=utf-8' });
3984
+ window.downloadNativeScript = function() {
3985
+ // Regenerate output first so the downloaded script always matches the latest wizard state.
3986
+ generateOutput();
3987
+ const script = window._nativeScript;
3988
+ if (!script) return;
3989
+ const blob = new Blob([script.content], { type: 'text/plain;charset=utf-8' });
3798
3990
  const url = URL.createObjectURL(blob);
3799
3991
  const a = document.createElement('a');
3800
3992
  a.href = url; a.download = script.name; a.style.display = 'none';