create-openclaw-bot 5.3.0 → 5.3.3

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
@@ -614,11 +614,34 @@
614
614
  step2.querySelectorAll('.channel-card[data-channel]').forEach((c) => c.classList.remove('channel-card--selected'));
615
615
  card.classList.add('channel-card--selected');
616
616
 
617
- // Show multi-bot panel for Telegram and Telegram+Zalo combo
618
617
  const multibotPanel = document.getElementById('multibot-panel');
619
- if (multibotPanel) {
620
- const showPanel = state.channel === 'telegram' || state.channel === 'telegram+zalo-personal';
621
- multibotPanel.style.display = showPanel ? '' : 'none';
618
+ if (state.channel === 'telegram+zalo-personal') {
619
+ // Combo: hide the bot-count selector (fixed 1 Telegram bot), show 2 per-channel tabs
620
+ if (multibotPanel) multibotPanel.style.display = 'none';
621
+ // Reset to single-bot and ensure 2 bots in array (index 0 = Telegram, index 1 = Zalo)
622
+ state.botCount = 1;
623
+ while (state.bots.length < 2) {
624
+ state.bots.push({ name: '', slashCmd: '', desc: '', provider: state.bots[0]?.provider || 'google', model: state.bots[0]?.model || 'google/gemini-2.5-flash', token: '', apiKey: '' });
625
+ }
626
+ // Hide global identity-grid (each tab has own name/desc)
627
+ const identityGrid = document.querySelector('.identity-grid');
628
+ if (identityGrid) {
629
+ const nameField = identityGrid.querySelector('.form-group:has(#cfg-name)');
630
+ const descField = identityGrid.querySelector('.form-group:has(#cfg-desc)');
631
+ if (nameField) nameField.style.display = 'none';
632
+ if (descField) descField.style.display = 'none';
633
+ }
634
+ } else {
635
+ // Not combo: show multibot panel for telegram only
636
+ if (multibotPanel) multibotPanel.style.display = state.channel === 'telegram' ? '' : 'none';
637
+ // Restore global identity-grid visibility if was hidden by combo
638
+ const identityGrid = document.querySelector('.identity-grid');
639
+ if (identityGrid) {
640
+ const nameField = identityGrid.querySelector('.form-group:has(#cfg-name)');
641
+ const descField = identityGrid.querySelector('.form-group:has(#cfg-desc)');
642
+ if (nameField) nameField.style.display = '';
643
+ if (descField) descField.style.display = '';
644
+ }
622
645
  }
623
646
 
624
647
  updateNavButtons();
@@ -737,8 +760,40 @@
737
760
  const slashGroup = document.getElementById('slash-cmd-group');
738
761
  if (!tabBar || !tabsEl) return;
739
762
 
763
+ const isCombo = state.channel === 'telegram+zalo-personal';
764
+
740
765
  tabBar.style.display = 'block';
741
766
 
767
+ // ── Combo mode: 2 fixed tabs (Telegram / Zalo Personal) ─────────────────
768
+ if (isCombo) {
769
+ tabsEl.style.display = 'flex';
770
+ if (labelEl) { labelEl.style.display = 'block'; labelEl.textContent = ''; }
771
+ if (slashGroup) slashGroup.style.display = 'none'; // slash cmd not relevant for Zalo
772
+
773
+ const COMBO_TABS = [
774
+ { icon: '📨', labelVi: 'Telegram', labelEn: 'Telegram' },
775
+ { icon: '💬', labelVi: 'Zalo Personal', labelEn: 'Zalo Personal' },
776
+ ];
777
+ const lang = document.getElementById('cfg-language')?.value || 'vi';
778
+
779
+ tabsEl.innerHTML = COMBO_TABS.map((tab, i) => {
780
+ const isActive = i === state.activeBotIndex;
781
+ const customName = state.bots[i]?.name;
782
+ const labelText = customName ? customName : (lang === 'vi' ? tab.labelVi : tab.labelEn);
783
+ const label = `${tab.icon} ${labelText}`;
784
+ return `<button onclick="window.__switchBotTab(${i})" style="
785
+ padding:7px 18px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;
786
+ border:1px solid ${isActive ? 'rgba(99,102,241,0.6)' : 'rgba(255,255,255,0.12)'};
787
+ background:${isActive ? 'rgba(99,102,241,0.2)' : 'transparent'};
788
+ color:${isActive ? 'var(--text-primary)' : 'var(--text-secondary)'};
789
+ transition:all 0.15s;">${label}</button>`;
790
+ }).join('');
791
+
792
+ syncBotTabMeta();
793
+ return;
794
+ }
795
+
796
+ // ── Normal mode ──────────────────────────────────────────────────────────
742
797
  if (state.botCount <= 1) {
743
798
  tabsEl.style.display = 'none';
744
799
  if (labelEl) labelEl.style.display = 'none';
@@ -1074,16 +1129,15 @@
1074
1129
  if (state.currentStep === 2 && !state.channel) isDisabled = true;
1075
1130
  // Step 3 (bot config): require at least one bot name
1076
1131
  if (state.currentStep === 3) {
1077
- if (state.botCount > 1) {
1078
- // Multi-bot: require name for the currently active bot tab
1079
- // Fallback to state.bots to handle re-render cases where DOM may not yet have the value
1132
+ const isCombo = state.channel === 'telegram+zalo-personal';
1133
+ if (state.botCount > 1 || isCombo) {
1134
+ // Multi-bot or combo: require name for the currently active bot tab
1080
1135
  const activeTab = state.activeBotIndex || 0;
1081
1136
  const tabNameVal = document.getElementById('cfg-bot-tab-name')?.value?.trim()
1082
1137
  || state.bots[activeTab]?.name?.trim();
1083
1138
  if (!tabNameVal) isDisabled = true;
1084
1139
  } else {
1085
1140
  // Single bot: require cfg-name or the shared tab name field
1086
- // Fallback to state.config.botName for cases where the DOM field was cleared on re-render
1087
1141
  const nameVal = document.getElementById('cfg-name')?.value?.trim()
1088
1142
  || document.getElementById('cfg-bot-tab-name')?.value?.trim()
1089
1143
  || state.config.botName?.trim();
@@ -1093,12 +1147,13 @@
1093
1147
  // Step 4 (api keys): require token/key
1094
1148
  if (state.currentStep === 4) {
1095
1149
  const provider = PROVIDERS[state.config.provider];
1096
- if (state.channel === 'telegram' && state.botCount > 1) {
1150
+ const hasTelegramCh = state.channel === 'telegram' || state.channel === 'telegram+zalo-personal';
1151
+ if (hasTelegramCh && state.botCount > 1) {
1097
1152
  // Multi-bot: check DOM first, fallback to state (works even before user types)
1098
1153
  const firstTokenEl = document.getElementById('key-bot-token-0');
1099
1154
  const firstTokenVal = firstTokenEl?.value?.trim() || state.bots[0]?.token?.trim() || '';
1100
1155
  if (!firstTokenVal) isDisabled = true;
1101
- } else if (state.channel === 'telegram' || state.channel === 'zalo-bot') {
1156
+ } else if (hasTelegramCh || state.channel === 'zalo-bot') {
1102
1157
  const botTokenEl = document.getElementById('key-bot-token');
1103
1158
  const botTokenVal = botTokenEl?.value?.trim() || state.config.botToken?.trim() || '';
1104
1159
  if (!botTokenVal) isDisabled = true;
@@ -1430,9 +1485,13 @@
1430
1485
  // Also save bot-tab-name → bots[0].name so both state locations stay in sync
1431
1486
  // Save bot-tab-name to the ACTIVE bot (not always bots[0])
1432
1487
  const tabName = document.getElementById('cfg-bot-tab-name')?.value?.trim();
1488
+ const isCombo = state.channel === 'telegram+zalo-personal';
1433
1489
  if (tabName && state.bots[state.activeBotIndex]) {
1434
1490
  state.bots[state.activeBotIndex].name = tabName;
1435
- if (state.botCount <= 1) state.config.botName = tabName;
1491
+ // For single-bot or combo (use Telegram tab = index 0 as primary name)
1492
+ if (state.botCount <= 1 || isCombo) {
1493
+ if (!isCombo || state.activeBotIndex === 0) state.config.botName = tabName;
1494
+ }
1436
1495
  } else if (state.config.botName && state.bots[0] && !state.bots[0].name) {
1437
1496
  state.bots[0].name = state.config.botName;
1438
1497
  }
@@ -1450,9 +1509,9 @@
1450
1509
  if (botTokenEl) state.config.botToken = botTokenEl.value;
1451
1510
  if (apiKeyEl) state.config.apiKey = apiKeyEl.value;
1452
1511
  if (pathEl) state.config.projectPath = pathEl.value;
1453
- if (state.botCount <= 1 && state.bots[state.activeBotIndex]) {
1454
- if (botTokenEl) state.bots[state.activeBotIndex].token = botTokenEl.value;
1455
- if (apiKeyEl) state.bots[state.activeBotIndex].apiKey = apiKeyEl.value;
1512
+ if (state.botCount <= 1 && state.bots[0]) {
1513
+ if (botTokenEl) state.bots[0].token = botTokenEl.value;
1514
+ if (apiKeyEl) state.bots[0].apiKey = apiKeyEl.value;
1456
1515
  }
1457
1516
 
1458
1517
  // Also save multi-bot tokens individually
@@ -1547,7 +1606,10 @@
1547
1606
  cHtml += `<div style="padding: 16px 20px; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; background: rgba(255,255,255,0.02);">`;
1548
1607
  cHtml += `<h3 style="margin: 0 0 12px; font-size: 15px; font-weight: 700; color: var(--text-primary);">${channelIcon} ${isVi ? 'Kênh chat' : 'Chat Channel'} — ${channelName}</h3>`;
1549
1608
 
1550
- if (state.channel === 'telegram') {
1609
+ const hasTelegramChKey = state.channel === 'telegram' || state.channel === 'telegram+zalo-personal';
1610
+ const hasZaloPersonalChKey = state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal';
1611
+
1612
+ if (hasTelegramChKey) {
1551
1613
  if (state.botCount > 1) {
1552
1614
  // Multi-bot: one token per bot
1553
1615
  cHtml += `<div style="display:flex;flex-direction:column;gap:12px;">`;
@@ -1571,14 +1633,16 @@
1571
1633
  <p class="form-group__hint">${isVi ? 'Lấy từ <a href="https://t.me/BotFather" target="_blank">@BotFather</a> trên Telegram' : 'Get from <a href="https://t.me/BotFather" target="_blank">@BotFather</a> on Telegram'}</p>
1572
1634
  </div>`;
1573
1635
  }
1574
- } else if (state.channel === 'zalo-bot') {
1636
+ }
1637
+ if (state.channel === 'zalo-bot') {
1575
1638
  cHtml += `<div class="form-group" style="margin: 0;">
1576
1639
  <label class="form-group__label" for="key-bot-token">🔑 Zalo Bot Token <span style="color: var(--danger, #ef4444);">*</span></label>
1577
1640
  <input type="text" class="form-input" id="key-bot-token" placeholder="Zalo Bot Token" style="font-family: monospace; font-size: 13px;" oninput="window.__validateKeys()">
1578
1641
  <p class="form-group__hint">${isVi ? 'Lấy từ <a href="https://developers.zalo.me" target="_blank">Zalo Bot Platform</a>' : 'Get from <a href="https://developers.zalo.me" target="_blank">Zalo Bot Platform</a>'}</p>
1579
1642
  </div>`;
1580
- } else if (state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal') {
1581
- cHtml += `<div style="display: flex; gap: 8px; align-items: flex-start; padding: 12px 14px; background: rgba(245,158,11,0.06); border: 1px solid rgba(245,158,11,0.2); border-radius: 8px; font-size: 13px; color: var(--warning); margin: 0;">
1643
+ }
1644
+ if (hasZaloPersonalChKey) {
1645
+ cHtml += `<div style="display: flex; gap: 8px; align-items: flex-start; padding: 12px 14px; background: rgba(245,158,11,0.06); border: 1px solid rgba(245,158,11,0.2); border-radius: 8px; font-size: 13px; color: var(--warning); margin: ${hasTelegramChKey ? '12px 0 0' : '0'};">
1582
1646
  <span style="font-size: 16px; margin-top: -2px;">⚠️</span>
1583
1647
  <span style="line-height: 1.5;">${isVi
1584
1648
  ? '<strong>Zalo Personal</strong> sử dụng unofficial API (zca-js). Tài khoản Zalo của bạn có thể bị hạn chế hoặc khóa. Chỉ nên dùng với tài khoản phụ.'
@@ -1657,7 +1721,8 @@
1657
1721
  }
1658
1722
 
1659
1723
  // Bot tokens
1660
- if (state.channel === 'telegram' && state.botCount > 1) {
1724
+ const hasTelegramForEnv = state.channel === 'telegram' || state.channel === 'telegram+zalo-personal';
1725
+ if (hasTelegramForEnv && state.botCount > 1) {
1661
1726
  // Multi-bot: one env var per bot
1662
1727
  lines.push('');
1663
1728
  lines.push('# Multi-bot Telegram tokens');
@@ -1702,6 +1767,8 @@
1702
1767
  }
1703
1768
 
1704
1769
 
1770
+ const openClawRuntimePackages = 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
1771
+
1705
1772
  // ========== Step 4: Generate Output ==========
1706
1773
  function generateOutput() {
1707
1774
  const ch = CHANNELS[state.channel];
@@ -1717,9 +1784,8 @@
1717
1784
 
1718
1785
  const is9Router = provider.isProxy;
1719
1786
  const isLocal = provider.isLocal;
1720
- const isTelegramMultiBot = state.botCount > 1 && state.channel === 'telegram';
1787
+ const isTelegramMultiBot = state.botCount > 1 && (state.channel === 'telegram' || state.channel === 'telegram+zalo-personal');
1721
1788
  const relayPluginSpec = 'openclaw-telegram-multibot-relay';
1722
- const openClawRuntimePackages = 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
1723
1789
 
1724
1790
  function buildRelayPluginInstallCommand(prefix) {
1725
1791
  return `${prefix} plugins install ${relayPluginSpec} 2>/dev/null || true`;
@@ -2159,7 +2225,7 @@ ${finalCmd}`;
2159
2225
 
2160
2226
  setOutput('out-dockerfile', dockerfile);
2161
2227
 
2162
- const isMultiBotWizard = state.botCount > 1 && state.channel === 'telegram';
2228
+ const isMultiBotWizard = state.botCount > 1 && (state.channel === 'telegram' || state.channel === 'telegram+zalo-personal');
2163
2229
 
2164
2230
  // 4. docker-compose.yml
2165
2231
  // extra_hosts always needed for browser (socat → host Chrome)
@@ -2490,12 +2556,13 @@ docker logs -f openclaw-bot${approveNote}`);
2490
2556
  // 7. Generate ALL workspace Markdown files
2491
2557
  // OpenClaw auto-injects these into agent context at the start of every session.
2492
2558
  // Hierarchy: per-agent files → global workspace files → config defaults.
2493
- const botName = isMultiBotWizard
2559
+ const isComboChannel = state.channel === 'telegram+zalo-personal';
2560
+ const botName = (isMultiBotWizard || isComboChannel)
2494
2561
  ? (state.bots[0]?.name || state.config.botName || 'Chat Bot')
2495
2562
  : (state.config.botName || 'Chat Bot');
2496
2563
  const lang = state.config.language || 'vi';
2497
2564
  const userPrompt = state.config.systemPrompt || '';
2498
- const descText = isMultiBotWizard
2565
+ const descText = (isMultiBotWizard || isComboChannel)
2499
2566
  ? (state.bots[0]?.desc || state.config.description || (lang === 'vi' ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'))
2500
2567
  : (state.config.description || (lang === 'vi' ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'));
2501
2568
 
@@ -3201,14 +3268,14 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3201
3268
  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);\"";
3202
3269
  }
3203
3270
 
3204
- function windowsHiddenNodeLaunch(targetPath, extraEnv = {}) {
3205
- function quotePowerShellSingle(value) {
3206
- return `'${String(value).replace(/'/g, "''")}'`;
3207
- }
3271
+ function windowsHiddenNodeLaunch(targetPath, extraEnv = {}, extraArgs = []) {
3272
+ // Set env vars via $env: prefix (PS5/PS7 compatible, -Environment flag is PS7+ only)
3208
3273
  const envAssignments = Object.entries(extraEnv)
3209
- .map(([key, value]) => `$env:${key}=${quotePowerShellSingle(String(value))}`)
3210
- .join('; ');
3211
- return `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${envAssignments ? `${envAssignments}; ` : ''}Start-Process -WindowStyle Hidden -FilePath (Get-Command node).Source -ArgumentList @('${targetPath.replace(/'/g, "''")}')"`;
3274
+ .map(([k, v]) => `$env:${k}='${String(v).replace(/'/g, "''")}'; `)
3275
+ .join('');
3276
+ const safePath = targetPath.replace(/\\/g, '\\\\').replace(/'/g, "''");
3277
+ const argList = [`'${safePath}'`, ...extraArgs.map(a => `'${String(a).replace(/'/g, "''")}' `)].join(',');
3278
+ return `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${envAssignments}Start-Process -WindowStyle Hidden -FilePath (Get-Command node).Source -ArgumentList @(${argList})"`;
3212
3279
  }
3213
3280
 
3214
3281
  // ─── Shared initializer (provider install) ───────────────────────────────
@@ -3216,15 +3283,45 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3216
3283
  if (is9Router) {
3217
3284
  if (shell === 'bat') {
3218
3285
  arr.push('call npm install -g 9router || goto :fail');
3219
- arr.push(`for /f "usebackq delims=" %%I in (\`${native9RouterServerEntryLookup()}\`) do set "NINE_ROUTER_ENTRY=%%I"`);
3220
- arr.push(windowsHiddenNodeLaunch('%NINE_ROUTER_ENTRY%', { PORT: '20128', HOSTNAME: '0.0.0.0', DATA_DIR: '%DATA_DIR%' }));
3221
- arr.push('timeout /t 5 /nobreak >nul');
3286
+ // Pre-create DATA_DIR and seed db.json with requireLogin:false BEFORE starting 9router.
3287
+ // If db.json is missing when 9router boots, it defaults to ~/.9router and requireLogin:true,
3288
+ // blocking the dashboard. Must be done BEFORE the `start` command below.
3289
+ arr.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
3290
+ arr.push('if not exist "%DATA_DIR%\\db.json" (');
3291
+ arr.push('> "%DATA_DIR%\\db.json" (');
3292
+ arr.push('echo({');
3293
+ arr.push('echo( "providerConnections": [],');
3294
+ arr.push('echo( "providerNodes": [],');
3295
+ arr.push('echo( "proxyPools": [],');
3296
+ arr.push('echo( "modelAliases": {},');
3297
+ arr.push('echo( "mitmAlias": {},');
3298
+ arr.push('echo( "combos": [],');
3299
+ arr.push('echo( "apiKeys": [],');
3300
+ arr.push('echo( "settings": {');
3301
+ arr.push('echo( "requireLogin": false,');
3302
+ arr.push('echo( "cloudEnabled": false,');
3303
+ arr.push('echo( "tunnelEnabled": false,');
3304
+ arr.push('echo( "comboStrategy": "fallback",');
3305
+ arr.push('echo( "mitmRouterBaseUrl": "http://localhost:20128"');
3306
+ arr.push('echo( },');
3307
+ arr.push('echo( "pricing": {}');
3308
+ arr.push('echo(}');
3309
+ arr.push(')');
3310
+ arr.push(')');
3311
+ // NOTE: -l (stdin listen mode) intentionally omitted — causes hangs in non-TTY cmd windows.
3312
+ // DATA_DIR passed via env so 9router reads from the project-local data folder.
3313
+ arr.push('start "9Router Dashboard" /min cmd /c "set DATA_DIR=%DATA_DIR%&& 9router -n -H 0.0.0.0 -p 20128 --skip-update"');
3314
+ arr.push('timeout /t 8 /nobreak >nul');
3222
3315
  } else {
3223
3316
  arr.push('npm install -g 9router');
3224
- arr.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
3225
- arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 DATA_DIR="$PWD/.9router" node "$NINE_ROUTER_ENTRY" >/tmp/9router.log 2>&1 &');
3317
+ // Pre-seed .9router/db.json before starting 9router (prevents requireLogin:true on first boot)
3318
+ arr.push('mkdir -p ".9router"');
3319
+ arr.push('if [ ! -f ".9router/db.json" ]; then cat > ".9router/db.json" << \'DBJSON\'\n{\n "providerConnections": [],\n "providerNodes": [],\n "proxyPools": [],\n "modelAliases": {},\n "mitmAlias": {},\n "combos": [],\n "apiKeys": [],\n "settings": {\n "requireLogin": false,\n "cloudEnabled": false,\n "tunnelEnabled": false,\n "comboStrategy": "fallback",\n "mitmRouterBaseUrl": "http://localhost:20128"\n },\n "pricing": {}\n}\nDBJSON\nfi');
3320
+ arr.push('NINE_ROUTER_BIN="$(command -v 9router)"');
3321
+ // NOTE: -l (stdin listen mode) intentionally omitted — causes hangs in non-TTY environments
3322
+ arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 DATA_DIR="$PWD/.9router" "$NINE_ROUTER_BIN" -n -H 0.0.0.0 -p 20128 --skip-update >/tmp/9router.log 2>&1 &');
3226
3323
  arr.push('nohup env DATA_DIR="$PWD/.9router" node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
3227
- arr.push('sleep 3');
3324
+ arr.push('sleep 5');
3228
3325
  }
3229
3326
  } else if (isOllama) {
3230
3327
  if (shell === 'bat') {
@@ -3461,7 +3558,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3461
3558
  // ─── Per-bot ENV content ──────────────────────────────────────────────────
3462
3559
  function botEnvContent(botIndex) {
3463
3560
  const bot = state.bots[botIndex] || {};
3464
- const botProvider = PROVIDERS[bot.provider] || provider;
3561
+ const botProvider = (provider && provider.isProxy) ? provider : (PROVIDERS[bot.provider] || provider);
3465
3562
  const lines = [];
3466
3563
  if (botProvider.isProxy) {
3467
3564
  lines.push('# 9Router: no API key needed');
@@ -3485,12 +3582,16 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3485
3582
  const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3486
3583
  const basePort = 18791 + botIndex;
3487
3584
  const groupId = state.groupId || '';
3488
- const botProvider = PROVIDERS[bot.provider] || provider;
3585
+
3586
+ // Force use global provider if proxy mode is chosen globally, else use bot specific provider
3587
+ const botProvider = (provider && provider.isProxy) ? provider : (PROVIDERS[bot.provider] || provider);
3588
+ const actualModel = botProvider.isProxy ? provider.models[0].id : (bot.model || state.config.model);
3589
+
3489
3590
  const cfg = {
3490
3591
  meta: { lastTouchedVersion: '2026.3.24' },
3491
3592
  agents: {
3492
3593
  defaults: {
3493
- model: { primary: bot.model || state.config.model },
3594
+ model: { primary: actualModel },
3494
3595
  compaction: { mode: 'safeguard' },
3495
3596
  timeoutSeconds: botProvider.isLocal ? 900 : 120,
3496
3597
  ...(botProvider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
@@ -3499,7 +3600,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3499
3600
  id: agentId,
3500
3601
  workspace: 'workspace',
3501
3602
  agentDir: `agents/${agentId}/agent`,
3502
- model: { primary: bot.model || state.config.model }
3603
+ model: { primary: actualModel }
3503
3604
  }],
3504
3605
  },
3505
3606
  ...(botProvider.isProxy ? {
@@ -3568,7 +3669,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3568
3669
  cfg.plugins = { ...(cfg.plugins || {}), slots: { ...((cfg.plugins && cfg.plugins.slots) || {}), memory: 'none' } };
3569
3670
  }
3570
3671
 
3571
- if (state.channel === 'telegram') {
3672
+ if (state.channel === 'telegram' || state.channel === 'telegram+zalo-personal') {
3572
3673
  cfg.channels.telegram = {
3573
3674
  enabled: true,
3574
3675
  dmPolicy: 'open',
@@ -3584,7 +3685,9 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3584
3685
  },
3585
3686
  };
3586
3687
  }
3587
- } else if (state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal') {
3688
+ }
3689
+
3690
+ if (state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal') {
3588
3691
  cfg.channels.zalouser = {
3589
3692
  enabled: true,
3590
3693
  dmPolicy: 'open',
@@ -3913,6 +4016,8 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3913
4016
  'set "OPENCLAW_STATE_DIR=%PROJECT_DIR%\\.openclaw"',
3914
4017
  'set "DATA_DIR=%PROJECT_DIR%\\.9router"',
3915
4018
  'set "PATH=%APPDATA%\\npm;%PATH%"',
4019
+ ':: Fix PowerShell ExecutionPolicy so .ps1 wrappers (openclaw, 9router) can run',
4020
+ 'powershell -NoProfile -Command "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force" >nul 2>&1',
3916
4021
  `echo === OpenClaw Setup — Windows${isDocker ? ' Docker' : ' Native'} ===`,
3917
4022
  'echo.',
3918
4023
  'echo [1/5] Kiem tra Node.js...',
@@ -3932,7 +4037,9 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3932
4037
  }
3933
4038
  if (pluginCmd) { lines.push('echo Cai plugins...'); lines.push(pluginCmd); }
3934
4039
  lines.push('if not exist "%OPENCLAW_HOME%" mkdir "%OPENCLAW_HOME%"');
3935
- lines.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
4040
+ // DATA_DIR creation + db.json pre-seeding is handled inside providerLines() for 9Router.
4041
+ // For non-9Router providers we still ensure the folder exists.
4042
+ if (!is9Router) lines.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
3936
4043
 
3937
4044
  if (isMultiBot) {
3938
4045
  lines.push('echo [4/5] Tao runtime multi-agent dung chung...');
@@ -3948,8 +4055,35 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3948
4055
  lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
3949
4056
  lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
3950
4057
  }
3951
- lines.push('echo [5/5] Khoi dong gateway multi-bot...');
3952
- lines.push('call openclaw gateway run');
4058
+ const needsZaloLoginMulti = state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal';
4059
+ if (needsZaloLoginMulti) {
4060
+ lines.push('echo [5/6] Khoi dong gateway (cua so moi) de chuan bi dang nhap Zalo...');
4061
+ // Use BAT-native `start` which inherits current env vars - no PS escaping needed
4062
+ lines.push('start "OpenClaw Gateway" cmd /c "openclaw gateway run"');
4063
+ lines.push('echo Cho gateway khoi dong (15 giay)...');
4064
+ lines.push('timeout /t 15 /nobreak >nul');
4065
+ lines.push('echo [6/6] Dang nhap Zalo - dang tao ma QR...');
4066
+ lines.push('openclaw channels login --channel zalouser --verbose');
4067
+ lines.push('echo.');
4068
+ // Copy QR PNG from TEMP to project dir so user can open it easily
4069
+ lines.push('set "QR_TMP=%TEMP%\\openclaw\\openclaw-zalouser-qr-default.png"');
4070
+ lines.push('if exist "%QR_TMP%" (');
4071
+ lines.push(' copy /y "%QR_TMP%" "%PROJECT_DIR%\\zalo-login-qr.png" >nul');
4072
+ lines.push(' echo ===================================================');
4073
+ lines.push(' echo Ma QR Zalo da duoc luu tai:');
4074
+ lines.push(' echo %PROJECT_DIR%\\zalo-login-qr.png');
4075
+ lines.push(' echo Mo file anh tren r dung Zalo quet de dang nhap!');
4076
+ lines.push(' echo ===================================================');
4077
+ lines.push(' start "" "%PROJECT_DIR%\\zalo-login-qr.png"');
4078
+ lines.push(') else (');
4079
+ lines.push(' echo Khong tim thay file QR. Vui long kiem tra cua so Gateway.');
4080
+ lines.push(')');
4081
+ lines.push('echo Gateway dang chay trong cua so rieng.');
4082
+ lines.push('echo De khoi dong lai: openclaw gateway run');
4083
+ } else {
4084
+ lines.push('echo [5/5] Khoi dong gateway multi-bot...');
4085
+ lines.push('call openclaw gateway run');
4086
+ }
3953
4087
  } else {
3954
4088
  lines.push('echo [4/5] Tao file cau hinh...');
3955
4089
  appendBatWriteCommands(lines, mapWindowsNativeFiles(botFiles(0)));
@@ -3964,8 +4098,28 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3964
4098
  lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
3965
4099
  lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
3966
4100
  }
3967
- lines.push('echo [5/5] Khoi dong bot...');
3968
- lines.push('call openclaw gateway run');
4101
+ const needsZaloLogin = state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal';
4102
+ if (needsZaloLogin) {
4103
+ lines.push('echo [5/6] Khoi dong gateway (cua so moi) de chuan bi dang nhap Zalo...');
4104
+ lines.push('start "OpenClaw Gateway" cmd /c "cd /d %PROJECT_DIR% && set OPENCLAW_HOME=%OPENCLAW_HOME% && set OPENCLAW_STATE_DIR=%OPENCLAW_HOME% && openclaw gateway run"');
4105
+ lines.push('echo Cho gateway khoi dong (15 giay)...');
4106
+ lines.push('timeout /t 15 /nobreak >nul');
4107
+ lines.push('echo [6/6] Dang nhap Zalo...');
4108
+ lines.push('echo.');
4109
+ lines.push('echo ============================================================');
4110
+ lines.push('echo Cua so CMD moi se mo de dang nhap Zalo.');
4111
+ lines.push('echo Hay lam theo huong dan trong cua so do:');
4112
+ lines.push('echo 1. Chon "Install Zalo Personal plugin?" (Enter)');
4113
+ lines.push('echo 2. Doi QR hien ra - mo app Zalo quet ma QR');
4114
+ lines.push('echo 3. Doi den khi thay "Login success" hoac token');
4115
+ lines.push('echo ============================================================');
4116
+ lines.push('start "Zalo Login" cmd /k "cd /d \"%PROJECT_DIR%\" && set OPENCLAW_HOME=%OPENCLAW_HOME% && set OPENCLAW_STATE_DIR=%OPENCLAW_HOME% && openclaw channels login --channel zalouser --verbose"');
4117
+ lines.push('echo Gateway dang chay trong cua so rieng.');
4118
+ lines.push('echo De khoi dong lai: openclaw gateway run');
4119
+ } else {
4120
+ lines.push('echo [5/5] Khoi dong bot...');
4121
+ lines.push('call openclaw gateway run');
4122
+ }
3969
4123
  }
3970
4124
 
3971
4125
  lines.push('goto :end');
@@ -4175,17 +4329,294 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
4175
4329
 
4176
4330
 
4177
4331
 
4332
+ // ========== Generate Uninstall Script ==========
4333
+ function generateUninstallScript() {
4334
+ const os = state.nativeOs || 'win';
4335
+ const isDocker = state.deployMode === 'docker';
4336
+ const projectDirRaw = document.getElementById('cfg-project-path')?.value?.trim() || '.';
4337
+ // Normalise to a sensible display path
4338
+ const projectDir = projectDirRaw;
4339
+ const absWin = projectDir.replace(/\//g, '\\');
4340
+ const absUnix = projectDir.replace(/\\/g, '/');
4341
+ const botName = (state.bots[0]?.name || 'openclaw').toLowerCase().replace(/[^a-z0-9]+/g, '-');
4342
+
4343
+ // ── Windows native .bat ────────────────────────────────────────────────────
4344
+ if (os === 'win' && !isDocker) {
4345
+ return {
4346
+ name: 'uninstall-openclaw-win.bat',
4347
+ content: `@echo off
4348
+ setlocal EnableExtensions
4349
+ chcp 65001 >nul
4350
+ echo.
4351
+ echo ============================================================
4352
+ echo OpenClaw Uninstaller - Windows Native
4353
+ echo Project: ${absWin}
4354
+ echo ============================================================
4355
+ echo.
4356
+ echo [WARNING] This will:
4357
+ echo 1. Kill openclaw and 9router background processes
4358
+ echo 2. Uninstall global npm packages (openclaw, 9router)
4359
+ echo 3. Delete the project folder and all its data
4360
+ echo.
4361
+ set /p CONFIRM=Nhap YES de xac nhan xoa toan bo:
4362
+ if /i not "%CONFIRM%"=="YES" (
4363
+ echo Huy bo. Khong xoa gi ca.
4364
+ pause
4365
+ exit /b 0
4366
+ )
4367
+ echo.
4368
+ echo [1/4] Dang dung cac tien trinh openclaw va 9router...
4369
+ taskkill /F /IM openclaw.exe >nul 2>&1
4370
+ taskkill /F /IM 9router.exe >nul 2>&1
4371
+ powershell -NoProfile -Command "Get-Process node -ErrorAction SilentlyContinue | Where-Object { $_.Path -like '*${absWin.replace(/\/g, '\\\\')}*' } | Stop-Process -Force" >nul 2>&1
4372
+ powershell -NoProfile -Command "& { $p=@(18791,20128); foreach($port in $p){ $id=(netstat -ano | Select-String \":\$($port) \").Line -split ' +' | Select-Object -Last 1; if($id -and $id -ne '0'){ Stop-Process -Id $id -Force -ErrorAction SilentlyContinue } } }" >nul 2>&1
4373
+ echo OK: Tien trinh da dung.
4374
+ echo.
4375
+ echo [2/4] Dang go cai npm packages toan cau...
4376
+ set "PATH=%APPDATA%\npm;%PATH%"
4377
+ call npm uninstall -g openclaw 9router grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>nul
4378
+ echo OK: npm packages da duoc go cai.
4379
+ echo.
4380
+ echo [3/4] Xoa thu muc project...
4381
+ set "TARGET=${absWin}"
4382
+ if exist "%TARGET%" (
4383
+ rd /s /q "%TARGET%"
4384
+ echo OK: Da xoa %TARGET%
4385
+ ) else (
4386
+ echo INFO: Thu muc khong ton tai: %TARGET%
4387
+ )
4388
+ echo.
4389
+ echo [4/4] Xoa thu muc .9router trong Home (neu co)...
4390
+ if exist "%USERPROFILE%\.9router" (
4391
+ set /p CLEAN_HOME=Xoa ca %USERPROFILE%\.9router? (YES/no):
4392
+ if /i "%CLEAN_HOME%"=="YES" rd /s /q "%USERPROFILE%\.9router" >nul 2>&1
4393
+ )
4394
+ echo.
4395
+ echo ============================================================
4396
+ echo Go cai hoan tat!
4397
+ echo De cai lai: chay lai file setup hoac npx create-openclaw-bot
4398
+ echo ============================================================
4399
+ pause
4400
+ endlocal
4401
+ `
4402
+ };
4403
+ }
4404
+
4405
+ // ── Windows Docker .bat ────────────────────────────────────────────────────
4406
+ if (os === 'win' && isDocker) {
4407
+ return {
4408
+ name: 'uninstall-openclaw-docker.bat',
4409
+ content: `@echo off
4410
+ setlocal EnableExtensions
4411
+ chcp 65001 >nul
4412
+ echo.
4413
+ echo ============================================================
4414
+ echo OpenClaw Uninstaller - Docker (Windows)
4415
+ echo Project: ${absWin}
4416
+ echo ============================================================
4417
+ echo.
4418
+ echo [WARNING] This will stop Docker containers and delete the project folder.
4419
+ echo.
4420
+ set /p CONFIRM=Nhap YES de xac nhan xoa toan bo:
4421
+ if /i not "%CONFIRM%"=="YES" (
4422
+ echo Huy bo. Khong xoa gi ca.
4423
+ pause
4424
+ exit /b 0
4425
+ )
4426
+ echo.
4427
+ echo [1/2] Dang dung Docker containers...
4428
+ cd /d "${absWin}\docker\openclaw" 2>nul && (
4429
+ docker compose down --volumes --remove-orphans 2>nul || docker-compose down --volumes --remove-orphans 2>nul
4430
+ echo OK: Containers da dung.
4431
+ ) || echo INFO: Khong tim thay docker compose.
4432
+ echo.
4433
+ echo [2/2] Xoa thu muc project...
4434
+ cd /d "%USERPROFILE%"
4435
+ if exist "${absWin}" (
4436
+ rd /s /q "${absWin}"
4437
+ echo OK: Da xoa ${absWin}
4438
+ )
4439
+ echo.
4440
+ echo ============================================================
4441
+ echo Go cai hoan tat! De cai lai: npx create-openclaw-bot@latest
4442
+ echo ============================================================
4443
+ pause
4444
+ endlocal
4445
+ `
4446
+ };
4447
+ }
4448
+
4449
+ // ── VPS / PM2 .sh ─────────────────────────────────────────────────────────
4450
+ if (os === 'vps') {
4451
+ return {
4452
+ name: 'uninstall-openclaw-vps.sh',
4453
+ content: `#!/usr/bin/env bash
4454
+ # ====== OpenClaw Uninstaller — VPS / Ubuntu Server (PM2) ======
4455
+ set -e
4456
+ PROJECT_DIR="${absUnix}"
4457
+ APP_NAME="${botName}"
4458
+
4459
+ echo ""
4460
+ echo "============================================================"
4461
+ echo " OpenClaw Uninstaller — VPS / Ubuntu Server"
4462
+ echo " Project: $PROJECT_DIR"
4463
+ echo " PM2 app: $APP_NAME"
4464
+ echo "============================================================"
4465
+ echo ""
4466
+ read -rp "Type YES to confirm full removal: " CONFIRM
4467
+ if [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi
4468
+
4469
+ echo "[1/5] Stopping PM2 processes..."
4470
+ if command -v pm2 &>/dev/null; then
4471
+ pm2 delete "$APP_NAME" "$APP_NAME-9router" "$APP_NAME-9router-sync" openclaw openclaw-multibot 2>/dev/null || true
4472
+ pm2 save --force 2>/dev/null || true
4473
+ fi
4474
+
4475
+ echo "[2/5] Killing leftover processes on ports 18791 / 20128..."
4476
+ for port in 18791 20128; do
4477
+ pid=$(lsof -ti tcp:\$port 2>/dev/null || true)
4478
+ [ -n "\$pid" ] && kill -9 \$pid 2>/dev/null || true
4479
+ done
4480
+
4481
+ echo "[3/5] Uninstalling npm packages..."
4482
+ npm uninstall -g openclaw 9router pm2 grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>/dev/null || true
4483
+
4484
+ echo "[4/5] Removing project directory..."
4485
+ [ -d "\$PROJECT_DIR" ] && rm -rf "\$PROJECT_DIR" && echo " OK: Deleted \$PROJECT_DIR" || echo " INFO: Not found."
4486
+
4487
+ echo "[5/5] Checking home-level .9router / .openclaw..."
4488
+ for dir in "\$HOME/.9router" "\$HOME/.openclaw"; do
4489
+ if [ -d "\$dir" ]; then
4490
+ read -rp "Delete \$dir ? (YES/no): " CLEAN
4491
+ [ "\$CLEAN" = "YES" ] && rm -rf "\$dir" && echo " OK: Deleted \$dir" || echo " Kept: \$dir"
4492
+ fi
4493
+ done
4494
+
4495
+ echo ""
4496
+ echo "============================================================"
4497
+ echo " Uninstall complete! Re-install: npx create-openclaw-bot@latest"
4498
+ echo "============================================================"
4499
+ `
4500
+ };
4501
+ }
4502
+
4503
+ // ── macOS / Linux Desktop .sh ──────────────────────────────────────────────
4504
+ if (os === 'linux' || os === 'linux-desktop') {
4505
+ const label = os === 'linux' ? 'macOS' : 'Linux Desktop';
4506
+ return {
4507
+ name: 'uninstall-openclaw.sh',
4508
+ content: `#!/usr/bin/env bash
4509
+ # ====== OpenClaw Uninstaller — ${label} (Native) ======
4510
+ set -e
4511
+ PROJECT_DIR="${absUnix}"
4512
+
4513
+ echo ""
4514
+ echo "============================================================"
4515
+ echo " OpenClaw Uninstaller — ${label} Native"
4516
+ echo " Project: $PROJECT_DIR"
4517
+ echo "============================================================"
4518
+ echo ""
4519
+ read -rp "Type YES to confirm full removal: " CONFIRM
4520
+ if [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi
4521
+
4522
+ echo "[1/4] Stopping openclaw and 9router processes..."
4523
+ pkill -f "openclaw gateway run" 2>/dev/null || true
4524
+ pkill -f "9router.*20128" 2>/dev/null || true
4525
+ pkill -f "9router-smart-route" 2>/dev/null || true
4526
+ pkill -f "\$PROJECT_DIR" 2>/dev/null || true
4527
+ for port in 18791 20128; do
4528
+ pid=$(lsof -ti tcp:\$port 2>/dev/null || true)
4529
+ [ -n "\$pid" ] && kill -9 \$pid 2>/dev/null || true
4530
+ done
4531
+
4532
+ echo "[2/4] Uninstalling npm packages..."
4533
+ npm uninstall -g openclaw 9router grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>/dev/null || true
4534
+ sudo npm uninstall -g openclaw 9router 2>/dev/null || true
4535
+
4536
+ echo "[3/4] Removing project directory..."
4537
+ [ -d "\$PROJECT_DIR" ] && rm -rf "\$PROJECT_DIR" && echo " OK: Deleted \$PROJECT_DIR" || echo " INFO: Not found."
4538
+
4539
+ echo "[4/4] Checking home-level .9router / .openclaw..."
4540
+ for dir in "\$HOME/.9router" "\$HOME/.openclaw"; do
4541
+ if [ -d "\$dir" ]; then
4542
+ read -rp "Delete \$dir ? (YES/no): " CLEAN
4543
+ [ "\$CLEAN" = "YES" ] && rm -rf "\$dir" && echo " OK: Deleted \$dir" || echo " Kept: \$dir"
4544
+ fi
4545
+ done
4546
+
4547
+ echo ""
4548
+ echo "============================================================"
4549
+ echo " Uninstall complete! Re-install: run setup script or npx create-openclaw-bot"
4550
+ echo "============================================================"
4551
+ `
4552
+ };
4553
+ }
4554
+
4555
+ // ── Docker macOS/Linux/VPS .sh ─────────────────────────────────────────────
4556
+ if (isDocker) {
4557
+ return {
4558
+ name: 'uninstall-openclaw-docker.sh',
4559
+ content: `#!/usr/bin/env bash
4560
+ # ====== OpenClaw Uninstaller — Docker ======
4561
+ set -e
4562
+ PROJECT_DIR="${absUnix}"
4563
+ DOCKER_DIR="$PROJECT_DIR/docker/openclaw"
4564
+
4565
+ echo ""
4566
+ echo "============================================================"
4567
+ echo " OpenClaw Uninstaller — Docker"
4568
+ echo " Project: $PROJECT_DIR"
4569
+ echo "============================================================"
4570
+ echo ""
4571
+ read -rp "Type YES to confirm full removal: " CONFIRM
4572
+ if [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi
4573
+
4574
+ echo "[1/3] Stopping Docker containers and removing volumes..."
4575
+ if [ -d "\$DOCKER_DIR" ] && command -v docker &>/dev/null; then
4576
+ cd "\$DOCKER_DIR"
4577
+ docker compose down --volumes --remove-orphans 2>/dev/null || docker-compose down --volumes --remove-orphans 2>/dev/null || true
4578
+ fi
4579
+
4580
+ echo "[2/3] Removing project directory..."
4581
+ [ -d "\$PROJECT_DIR" ] && rm -rf "\$PROJECT_DIR" && echo " OK: Deleted \$PROJECT_DIR" || echo " INFO: Not found."
4582
+
4583
+ echo "[3/3] Checking home-level .openclaw..."
4584
+ if [ -d "\$HOME/.openclaw" ]; then
4585
+ read -rp "Delete \$HOME/.openclaw? (YES/no): " CLEAN
4586
+ [ "\$CLEAN" = "YES" ] && rm -rf "\$HOME/.openclaw" && echo " OK." || echo " Kept."
4587
+ fi
4588
+
4589
+ echo ""
4590
+ echo "============================================================"
4591
+ echo " Uninstall complete! Re-install: npx create-openclaw-bot@latest"
4592
+ echo "============================================================"
4593
+ `
4594
+ };
4595
+ }
4596
+
4597
+ return null;
4598
+ }
4599
+
4600
+ function _triggerDownload(filename, content, mimeType) {
4601
+ const blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' });
4602
+ const url = URL.createObjectURL(blob);
4603
+ const a = document.createElement('a');
4604
+ a.href = url; a.download = filename; a.style.display = 'none';
4605
+ document.body.appendChild(a); a.click();
4606
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 1500);
4607
+ }
4608
+
4178
4609
  window.downloadNativeScript = function() {
4179
4610
  // Regenerate output first so the downloaded script always matches the latest wizard state.
4180
4611
  generateOutput();
4181
4612
  const script = window._nativeScript;
4182
4613
  if (!script) return;
4183
- const blob = new Blob([script.content], { type: 'text/plain;charset=utf-8' });
4184
- const url = URL.createObjectURL(blob);
4185
- const a = document.createElement('a');
4186
- a.href = url; a.download = script.name; a.style.display = 'none';
4187
- document.body.appendChild(a); a.click();
4188
- setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 1000);
4614
+ _triggerDownload(script.name, script.content, 'text/plain;charset=utf-8');
4615
+ // Also download the matching uninstall script right after
4616
+ const uninstall = generateUninstallScript();
4617
+ if (uninstall) {
4618
+ setTimeout(() => _triggerDownload(uninstall.name, uninstall.content, 'text/plain;charset=utf-8'), 600);
4619
+ }
4189
4620
  };
4190
4621
 
4191
4622
  // ========== Generate Windows Auto Setup .bat ==========
@@ -4244,10 +4675,23 @@ New-Item -ItemType Directory -Force -Path "$projectDir" | Out-Null
4244
4675
  // [3/4] Docker build
4245
4676
  ps += `# [3/4] Docker build
4246
4677
  Write-Host "[3/4] ${isVi ? 'Build Docker image (co the mat vai phut)...' : 'Building Docker image (may take a few minutes)...'}" -ForegroundColor Yellow
4678
+ \$dockerExe = \$null
4679
+ try { \$dockerExe = (Get-Command 'docker' -ErrorAction Stop).Source } catch {}
4680
+ if (-not \$dockerExe) {
4681
+ Write-Host " ${isVi ? 'Khong tim thay Docker. Tai Docker Desktop tai: https://www.docker.com/products/docker-desktop' : 'Docker not found. Download Docker Desktop at: https://www.docker.com/products/docker-desktop'}" -ForegroundColor Red
4682
+ Read-Host "Press Enter to exit"
4683
+ exit 1
4684
+ }
4685
+ & \$dockerExe info 2>&1 | Out-Null
4686
+ if (\$LASTEXITCODE -ne 0) {
4687
+ Write-Host " ${isVi ? 'Docker Desktop chua chay. Mo Docker Desktop roi thu lai.' : 'Docker Desktop is not running. Start Docker Desktop, then re-run this script.'}" -ForegroundColor Red
4688
+ Read-Host "Press Enter to exit"
4689
+ exit 1
4690
+ }
4247
4691
  Set-Location "$projectDir\\docker\\openclaw"
4248
- docker compose build
4249
- if ($LASTEXITCODE -ne 0) {
4250
- Write-Host " ❌ ${isVi ? 'Docker build that bai. Docker Desktop da chay chua?' : 'Docker build failed. Is Docker Desktop running?'}" -ForegroundColor Red
4692
+ & \$dockerExe compose build
4693
+ if (\$LASTEXITCODE -ne 0) {
4694
+ Write-Host " ❌ ${isVi ? 'Docker build that bai. Xem log phia tren.' : 'Docker build failed. Check the log above.'}" -ForegroundColor Red
4251
4695
  Read-Host "${isVi ? 'Nhan Enter de thoat' : 'Press Enter to exit'}"
4252
4696
  exit 1
4253
4697
  }
@@ -4258,7 +4702,7 @@ Write-Host " ✅ ${isVi ? 'Docker image da build' : 'Docker image built'}" -For
4258
4702
  // [4/4] Docker up
4259
4703
  ps += `# [4/4] Start bot
4260
4704
  Write-Host "[4/4] ${isVi ? 'Khoi dong bot...' : 'Starting bot...'}" -ForegroundColor Yellow
4261
- docker compose up -d
4705
+ & \$dockerExe compose up -d
4262
4706
  Write-Host " ✅ ${isVi ? 'Bot dang chay!' : 'Bot is running!'}" -ForegroundColor Green
4263
4707
 
4264
4708
  Write-Host ""
@@ -4270,7 +4714,7 @@ Write-Host " 🎉 ${isVi ? 'Setup hoan tat!' : 'Setup complete!'}" -ForegroundC
4270
4714
  if (is9Router) {
4271
4715
  ps += `Write-Host " ${isVi ? 'Mo http://localhost:30128/dashboard de login OAuth' : 'Open http://localhost:30128/dashboard to login OAuth'}" -ForegroundColor White\n`;
4272
4716
  }
4273
- if (state.channel === 'zalo-personal') {
4717
+ if (state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal') {
4274
4718
  ps += `Write-Host " ${isVi ? 'Chay: docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose' : 'Run: docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose'}" -ForegroundColor White\n`;
4275
4719
  ps += `Write-Host " ${isVi ? 'QR se nam tai /tmp/openclaw/openclaw-zalouser-qr-default.png' : 'QR will be written to /tmp/openclaw/openclaw-zalouser-qr-default.png'}" -ForegroundColor DarkGray\n`;
4276
4720
  ps += `Write-Host " ${isVi ? 'Copy QR ra ngoai: docker compose cp ai-bot:/tmp/openclaw/openclaw-zalouser-qr-default.png ./zalo-login-qr.png' : 'Copy the QR out: docker compose cp ai-bot:/tmp/openclaw/openclaw-zalouser-qr-default.png ./zalo-login-qr.png'}" -ForegroundColor DarkGray\n`;
@@ -4524,6 +4968,18 @@ echo ""
4524
4968
  zip.file(path, content);
4525
4969
  });
4526
4970
 
4971
+ // Include the native setup script (if native mode)
4972
+ const nativeScript = window._nativeScript;
4973
+ if (nativeScript && nativeScript.name && nativeScript.content) {
4974
+ zip.file(nativeScript.name, nativeScript.content);
4975
+ }
4976
+
4977
+ // Include the matching uninstall script
4978
+ const uninstall = generateUninstallScript();
4979
+ if (uninstall && uninstall.name && uninstall.content) {
4980
+ zip.file(uninstall.name, uninstall.content);
4981
+ }
4982
+
4527
4983
  const blob = await zip.generateAsync({ type: 'blob' });
4528
4984
  const url = URL.createObjectURL(blob);
4529
4985
  const a = document.createElement('a');