create-openclaw-bot 5.3.0 → 5.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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`;
@@ -1867,7 +1933,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1867
1933
  },
1868
1934
  list: [{
1869
1935
  id: agentId,
1870
- workspace: 'workspace',
1936
+ workspace: `.openclaw/workspace-${agentId}`,
1871
1937
  agentDir: `agents/${agentId}/agent`,
1872
1938
  model: { primary: state.config.model, fallbacks: [] },
1873
1939
  }],
@@ -1984,7 +2050,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1984
2050
  clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
1985
2051
  id: meta.agentId,
1986
2052
  name: meta.name,
1987
- workspace: meta.workspaceDir,
2053
+ workspace: '.openclaw/' + meta.workspaceDir,
1988
2054
  agentDir: `agents/${meta.agentId}/agent`,
1989
2055
  model: { primary: state.config.model, fallbacks: [] },
1990
2056
  }));
@@ -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
 
@@ -2955,7 +3022,7 @@ fi
2955
3022
  botConfig.agents.defaults.model = { primary: state.config.model, fallbacks: [] };
2956
3023
  botConfig.agents.list = [{
2957
3024
  id: botAgentId,
2958
- workspace: 'workspace',
3025
+ workspace: `.openclaw/workspace-${botAgentId}`,
2959
3026
  agentDir: `agents/${botAgentId}/agent`,
2960
3027
  model: { primary: state.config.model, fallbacks: [] },
2961
3028
  }];
@@ -3173,6 +3240,7 @@ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
3173
3240
  .filter((skill) => skill && skill.id !== 'scheduler' && skill.slug && skill.slug !== 'browser-automation');
3174
3241
  const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
3175
3242
  const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
3243
+ const isComboChannel = state.channel === 'telegram+zalo-personal';
3176
3244
  const projectDir = state.config.projectPath || '.';
3177
3245
  const todayStamp = new Date().toISOString().slice(0, 10);
3178
3246
 
@@ -3201,30 +3269,76 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3201
3269
  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
3270
  }
3203
3271
 
3204
- function windowsHiddenNodeLaunch(targetPath, extraEnv = {}) {
3205
- function quotePowerShellSingle(value) {
3206
- return `'${String(value).replace(/'/g, "''")}'`;
3207
- }
3272
+ function windowsHiddenNodeLaunch(targetPath, extraEnv = {}, extraArgs = []) {
3273
+ // Set env vars via $env: prefix (PS5/PS7 compatible, -Environment flag is PS7+ only)
3208
3274
  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, "''")}')"`;
3275
+ .map(([k, v]) => `$env:${k}='${String(v).replace(/'/g, "''")}'; `)
3276
+ .join('');
3277
+ const safePath = targetPath.replace(/\\/g, '\\\\').replace(/'/g, "''");
3278
+ const argList = [`'${safePath}'`, ...extraArgs.map(a => `'${String(a).replace(/'/g, "''")}' `)].join(',');
3279
+ return `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${envAssignments}Start-Process -WindowStyle Hidden -FilePath (Get-Command node).Source -ArgumentList @(${argList})"`;
3212
3280
  }
3213
3281
 
3214
3282
  // ─── Shared initializer (provider install) ───────────────────────────────
3215
3283
  function providerLines(arr, shell) {
3216
3284
  if (is9Router) {
3217
3285
  if (shell === 'bat') {
3286
+ arr.push(':: Dung 9Router dang chay (neu co) - tranh loi EBUSY khi npm cap nhat file dang bi lock');
3287
+ arr.push('wmic process where "Name=\'node.exe\' and CommandLine like \'%%9router%%\'" delete >nul 2>&1');
3288
+ arr.push('wmic process where "Name=\'cmd.exe\' and CommandLine like \'%%9router%%\'" delete >nul 2>&1');
3289
+ arr.push('timeout /t 3 /nobreak >nul');
3218
3290
  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');
3291
+ arr.push('echo [OK] 9Router da duoc cai dat thanh cong.');
3292
+ // Pre-create DATA_DIR and seed db.json with requireLogin:false BEFORE starting 9router.
3293
+ // If db.json is missing when 9router boots, it defaults to ~/.9router and requireLogin:true,
3294
+ // blocking the dashboard. Must be done BEFORE the `start` command below.
3295
+ arr.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
3296
+ arr.push('if not exist "%DATA_DIR%\\db.json" (');
3297
+ arr.push('> "%DATA_DIR%\\db.json" (');
3298
+ arr.push('echo({');
3299
+ arr.push('echo( "providerConnections": [],');
3300
+ arr.push('echo( "providerNodes": [],');
3301
+ arr.push('echo( "proxyPools": [],');
3302
+ arr.push('echo( "modelAliases": {},');
3303
+ arr.push('echo( "mitmAlias": {},');
3304
+ arr.push('echo( "combos": [],');
3305
+ arr.push('echo( "apiKeys": [],');
3306
+ arr.push('echo( "settings": {');
3307
+ arr.push('echo( "requireLogin": false,');
3308
+ arr.push('echo( "cloudEnabled": false,');
3309
+ arr.push('echo( "tunnelEnabled": false,');
3310
+ arr.push('echo( "comboStrategy": "fallback",');
3311
+ arr.push('echo( "mitmRouterBaseUrl": "http://localhost:20128"');
3312
+ arr.push('echo( },');
3313
+ arr.push('echo( "pricing": {}');
3314
+ arr.push('echo(}');
3315
+ arr.push(')');
3316
+ arr.push(')');
3317
+ // Launch 9Router as a fully detached process via PowerShell Start-Process -WindowStyle Hidden.
3318
+ // Using `start ... cmd /c "...--tray"` caused 9Router to die when the CMD window was closed
3319
+ // because cmd /c is a child of the started CMD, which is killed when that window closes.
3320
+ // Start-Process with WindowStyle Hidden creates a truly independent process that survives
3321
+ // even if all visible terminal windows are closed.
3322
+ // Write a temp .ps1 launcher to avoid CMD->PS quoting issues.
3323
+ // Start-Process cannot run .cmd files directly — cmd.exe must be the FilePath.
3324
+ arr.push('echo Khoi dong 9Router (background)...');
3325
+ arr.push('echo $env:DATA_DIR = \'%DATA_DIR%\' > "%TEMP%\\oc-start9r.ps1"');
3326
+ arr.push('echo $b = Join-Path $env:APPDATA \'npm\\9router.cmd\' >> "%TEMP%\\oc-start9r.ps1"');
3327
+ arr.push('echo if ^(-not ^(Test-Path $b^)^) { $b = Join-Path $env:APPDATA \'npm\\9router\' } >> "%TEMP%\\oc-start9r.ps1"');
3328
+ arr.push(`echo Start-Process 'cmd.exe' -WindowStyle Hidden -WorkingDirectory '${projectDir}' -ArgumentList ^('/c "' + $b + '" -n -H 0.0.0.0 -p 20128 --skip-update'^) >> "%TEMP%\\oc-start9r.ps1"`);
3329
+ arr.push('powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\\oc-start9r.ps1"');
3330
+ arr.push('del "%TEMP%\\oc-start9r.ps1" >nul 2>&1');
3331
+ arr.push('timeout /t 8 /nobreak >nul');
3222
3332
  } else {
3223
3333
  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 &');
3226
- 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');
3334
+ // Pre-seed .9router/db.json before starting 9router (prevents requireLogin:true on first boot)
3335
+ arr.push('mkdir -p ".9router"');
3336
+ 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');
3337
+ arr.push('NINE_ROUTER_BIN="$(command -v 9router)"');
3338
+ // NOTE: -l (stdin listen mode) intentionally omitted — causes hangs in non-TTY environments
3339
+ 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 &');
3340
+ arr.push('nohup env DATA_DIR="$PWD/.9router" node ./.9router/9router-smart-route-sync.js > /tmp/9router-sync.log 2>&1 &');
3341
+ arr.push('sleep 5');
3228
3342
  }
3229
3343
  } else if (isOllama) {
3230
3344
  if (shell === 'bat') {
@@ -3340,7 +3454,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3340
3454
  list: multiBotAgentMetas.map((meta) => ({
3341
3455
  id: meta.agentId,
3342
3456
  name: meta.name,
3343
- workspace: meta.workspaceDir,
3457
+ workspace: '.openclaw/' + meta.workspaceDir,
3344
3458
  agentDir: `agents/${meta.agentId}/agent`,
3345
3459
  model: { primary: state.config.model, fallbacks: [] },
3346
3460
  })),
@@ -3407,7 +3521,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3407
3521
  '.openclaw/auth-profiles.json': sharedNativeAuthProfilesContent(),
3408
3522
  'TELEGRAM-POST-INSTALL.md': buildTelegramPostInstallChecklist(),
3409
3523
  };
3410
- if (is9Router) files['.openclaw/9router-smart-route-sync.js'] = native9RouterSyncScriptContent();
3524
+ if (is9Router) files['.9router/9router-smart-route-sync.js'] = native9RouterSyncScriptContent();
3411
3525
  const teamMd = isVi
3412
3526
  ? `# Doi Bot\n\n${multiBotAgentMetas.map((meta) => `## ${meta.name}\n- Vai tro: ${meta.desc}\n- Agent ID: \`${meta.agentId}\`\n- Telegram accountId: \`${meta.accountId}\`\n- Slash command: ${meta.slashCmd || '_(chua co)_'}\n- Tinh cach: ${meta.persona || '_(khong ghi ro)_'}`).join('\n\n')}\n\n## Quy uoc phoi hop\n- Tat ca bot trong doi biet ro vai tro cua nhau.\n- Neu user bao ban hoi mot bot khac, hay dung agent-to-agent noi bo thay vi doi Telegram chuyen tin cua bot.\n- Bot mo loi chi noi 1 cau ngan, sau do chuyen turn noi bo cho bot dich.\n- Bot dich phai tra loi cong khai bang chinh Telegram account cua minh trong cung chat/thread hien tai.\n- Neu can fallback, chi bot mo loi moi duoc phep tom tat thay.`
3413
3527
  : `# Bot Team\n\n${multiBotAgentMetas.map((meta) => `## ${meta.name}\n- Role: ${meta.desc}\n- Agent ID: \`${meta.agentId}\`\n- Telegram accountId: \`${meta.accountId}\`\n- Slash command: ${meta.slashCmd || '_(not set)_'}\n- Persona: ${meta.persona || '_(not specified)_'}`).join('\n\n')}\n\n## Coordination Rules\n- Every bot knows the full roster.\n- If the user asks you to consult another bot, use internal agent-to-agent handoff instead of waiting for Telegram bot-to-bot delivery.\n- The caller bot only sends one short opener, then hands off internally.\n- The target bot must publish the real answer with its own Telegram account in the same chat/thread.\n- If a fallback is needed, only the caller bot may summarize on behalf of the target.`;
@@ -3461,7 +3575,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3461
3575
  // ─── Per-bot ENV content ──────────────────────────────────────────────────
3462
3576
  function botEnvContent(botIndex) {
3463
3577
  const bot = state.bots[botIndex] || {};
3464
- const botProvider = PROVIDERS[bot.provider] || provider;
3578
+ const botProvider = (provider && provider.isProxy) ? provider : (PROVIDERS[bot.provider] || provider);
3465
3579
  const lines = [];
3466
3580
  if (botProvider.isProxy) {
3467
3581
  lines.push('# 9Router: no API key needed');
@@ -3485,21 +3599,25 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3485
3599
  const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3486
3600
  const basePort = 18791 + botIndex;
3487
3601
  const groupId = state.groupId || '';
3488
- const botProvider = PROVIDERS[bot.provider] || provider;
3602
+
3603
+ // Force use global provider if proxy mode is chosen globally, else use bot specific provider
3604
+ const botProvider = (provider && provider.isProxy) ? provider : (PROVIDERS[bot.provider] || provider);
3605
+ const actualModel = botProvider.isProxy ? provider.models[0].id : (bot.model || state.config.model);
3606
+
3489
3607
  const cfg = {
3490
3608
  meta: { lastTouchedVersion: '2026.3.24' },
3491
3609
  agents: {
3492
3610
  defaults: {
3493
- model: { primary: bot.model || state.config.model },
3611
+ model: { primary: actualModel },
3494
3612
  compaction: { mode: 'safeguard' },
3495
3613
  timeoutSeconds: botProvider.isLocal ? 900 : 120,
3496
3614
  ...(botProvider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
3497
3615
  },
3498
3616
  list: [{
3499
3617
  id: agentId,
3500
- workspace: 'workspace',
3618
+ workspace: `.openclaw/workspace-${agentId}`,
3501
3619
  agentDir: `agents/${agentId}/agent`,
3502
- model: { primary: bot.model || state.config.model }
3620
+ model: { primary: actualModel }
3503
3621
  }],
3504
3622
  },
3505
3623
  ...(botProvider.isProxy ? {
@@ -3568,7 +3686,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3568
3686
  cfg.plugins = { ...(cfg.plugins || {}), slots: { ...((cfg.plugins && cfg.plugins.slots) || {}), memory: 'none' } };
3569
3687
  }
3570
3688
 
3571
- if (state.channel === 'telegram') {
3689
+ if (state.channel === 'telegram' || state.channel === 'telegram+zalo-personal') {
3572
3690
  cfg.channels.telegram = {
3573
3691
  enabled: true,
3574
3692
  dmPolicy: 'open',
@@ -3584,7 +3702,9 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3584
3702
  },
3585
3703
  };
3586
3704
  }
3587
- } else if (state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal') {
3705
+ }
3706
+
3707
+ if (state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal') {
3588
3708
  cfg.channels.zalouser = {
3589
3709
  enabled: true,
3590
3710
  dmPolicy: 'open',
@@ -3767,6 +3887,7 @@ You are **${botName}**, ${botDesc.toLowerCase()}.
3767
3887
  - Prefer English unless user uses another language
3768
3888
  - When asked your name: _"I'm ${botName}"_
3769
3889
  - Never fabricate information`;
3890
+ const _secRules = state.config.securityRules || DEFAULT_SECURITY_RULES[isVi ? 'vi' : 'en'];
3770
3891
  const extraAgentsMd = isVi
3771
3892
  ? `\n\n## Khi nao nen tra loi\n- Trong group, chi tra loi khi tin nhan co alias cua ban: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')} hoac username Telegram cua ban.\n- Neu tin nhan khong goi ban, hay im lang hoan toan.\n- Neu tin nhan chi goi ro bot khac ${otherBotNames.length ? otherBotNames.map((name) => `\`${name}\``).join(', ') : '`bot khac`'} thi khong cuop loi.\n- Khi da biet user dang goi ban, hay tha reaction co dinh \`👍\` truoc roi moi tra loi bang text. Khong dung emoji khac.\n- Khi can phoi hop noi bo, dung dung agent id ky thuat trong \`TEAM.md\`, khong dung ten hien thi.\n- Khi hoi ve vai tro cac bot, dung \`TEAM.md\` lam nguon su that.`
3772
3893
  : `\n\n## When To Reply\n- In group chats, only reply when the message contains one of your aliases: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')} or your Telegram username.\n- If the message is not calling you, stay completely silent.\n- If the message is clearly calling another bot such as ${otherBotNames.length ? otherBotNames.map((name) => `\`${name}\``).join(', ') : '`another bot`'}, do not hijack it.\n- Once you know the user is calling you, add the fixed reaction \`👍\` first, then send the text reply. Do not use any other reaction emoji.\n- When you need internal coordination, use the exact technical agent id from \`TEAM.md\`, not the display name.\n- Use \`TEAM.md\` as the source of truth for team roles.`;
@@ -3795,7 +3916,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(Chưa có ski
3795
3916
  ## Quy ước
3796
3917
  - Ưu tiên dùng tool thay vì đoán
3797
3918
  - Browser: dùng khi user yêu cầu thao tác web
3798
- - Memory: cập nhật khi biết thông tin quan trọng`
3919
+ - Memory: cập nhật khi biết thông tin quan trọng\n\n## Ghi chú thiết lập của bạn\n\nGhi lại cấu hình riêng của môi trường bạn, ví dụ:\n- Tên thiết bị, camera, SSH hosts\n- Giọng nói ưa thích (TTS)\n- Alias và shortcut\n\n---\n\nThêm ghi chú nào giúp ích cho công việc của bạn.`
3799
3920
  : `# Tool Usage Guide
3800
3921
 
3801
3922
  ## Installed Skills
@@ -3804,7 +3925,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3804
3925
  ## Conventions
3805
3926
  - Prefer tools over guessing
3806
3927
  - Use Browser for explicit web tasks
3807
- - Update Memory when important user info appears`;
3928
+ - Update Memory when important user info appears\n\n## Your Setup Notes\n\nRecord environment-specific config, e.g.:\n- Device names, cameras, SSH hosts\n- Preferred TTS voice\n- Aliases and shortcuts\n\n---\n\nAdd whatever helps you do your job.`;
3808
3929
  const memoryMd = isVi
3809
3930
  ? `# Bộ nhớ dài hạn
3810
3931
 
@@ -3817,7 +3938,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3817
3938
  const files = {
3818
3939
  'IDENTITY.md': identityMd,
3819
3940
  'SOUL.md': soulMd,
3820
- 'AGENTS.md': agentsMd + extraAgentsMd,
3941
+ 'AGENTS.md': agentsMd + extraAgentsMd + '\n\n' + _secRules,
3821
3942
  'TEAM.md': teamMd,
3822
3943
  'USER.md': userMd,
3823
3944
  'TOOLS.md': toolsMd,
@@ -3842,11 +3963,11 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3842
3963
  files[`${base}/.openclaw/openclaw.json`] = botConfigContent(botIndex);
3843
3964
  files[`${base}/.openclaw/exec-approvals.json`] = botExecApprovalsContent(botIndex);
3844
3965
  files[`${base}/.openclaw/auth-profiles.json`] = botAuthProfilesContent(botIndex);
3845
- if (is9Router) files[`${base}/.openclaw/9router-smart-route-sync.js`] = native9RouterSyncScriptContent();
3966
+ if (is9Router) files[`${base}/.9router/9router-smart-route-sync.js`] = native9RouterSyncScriptContent();
3846
3967
  files[`${base}/.openclaw/agents/${agentId}.yaml`] = botAgentYamlContent(botIndex);
3847
3968
  files[`${base}/.openclaw/agents/${agentId}/agent/auth-profiles.json`] = botAuthProfilesContent(botIndex);
3848
3969
  Object.entries(botWorkspaceFiles(botIndex)).forEach(([name, content]) => {
3849
- files[`${base}/.openclaw/workspace/${name}`] = content;
3970
+ files[`${base}/.openclaw/workspace-${agentId}/${name}`] = content;
3850
3971
  });
3851
3972
  return files;
3852
3973
  }
@@ -3888,6 +4009,9 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3888
4009
  return Object.fromEntries(Object.entries(files).map(([relPath, content]) => {
3889
4010
  const normalized = relPath.replace(/\\/g, '/');
3890
4011
  if (normalized === '.env') return ['%PROJECT_DIR%\\.env', content];
4012
+ if (normalized.startsWith('.9router/')) {
4013
+ return [`%DATA_DIR%\\${normalized.slice('.9router/'.length).replace(/\//g, '\\')}`, content];
4014
+ }
3891
4015
  if (normalized.startsWith('.openclaw/')) {
3892
4016
  return [`%OPENCLAW_HOME%\\${normalized.slice('.openclaw/'.length).replace(/\//g, '\\')}`, content];
3893
4017
  }
@@ -3913,12 +4037,15 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3913
4037
  'set "OPENCLAW_STATE_DIR=%PROJECT_DIR%\\.openclaw"',
3914
4038
  'set "DATA_DIR=%PROJECT_DIR%\\.9router"',
3915
4039
  'set "PATH=%APPDATA%\\npm;%PATH%"',
4040
+ ':: Fix PowerShell ExecutionPolicy so .ps1 wrappers (openclaw, 9router) can run',
4041
+ 'powershell -NoProfile -Command "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force" >nul 2>&1',
3916
4042
  `echo === OpenClaw Setup — Windows${isDocker ? ' Docker' : ' Native'} ===`,
3917
4043
  'echo.',
3918
4044
  'echo [1/5] Kiem tra Node.js...',
3919
4045
  'where node >nul 2>&1 || (echo ERROR: Node.js chua cai! Tai tai: https://nodejs.org && pause && exit /b 1)',
3920
4046
  'echo [2/5] Cai OpenClaw CLI...',
3921
4047
  `call npm install -g openclaw@2026.4.5 ${openClawRuntimePackages} || goto :fail`,
4048
+ 'echo [OK] OpenClaw da duoc cai dat thanh cong.',
3922
4049
  ];
3923
4050
  providerLines(lines, 'bat');
3924
4051
  if (hasBrowser) {
@@ -3932,12 +4059,14 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3932
4059
  }
3933
4060
  if (pluginCmd) { lines.push('echo Cai plugins...'); lines.push(pluginCmd); }
3934
4061
  lines.push('if not exist "%OPENCLAW_HOME%" mkdir "%OPENCLAW_HOME%"');
3935
- lines.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
4062
+ // DATA_DIR creation + db.json pre-seeding is handled inside providerLines() for 9Router.
4063
+ // For non-9Router providers we still ensure the folder exists.
4064
+ if (!is9Router) lines.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
3936
4065
 
3937
4066
  if (isMultiBot) {
3938
4067
  lines.push('echo [4/5] Tao runtime multi-agent dung chung...');
3939
4068
  appendBatWriteCommands(lines, mapWindowsNativeFiles(sharedNativeFileMap()));
3940
- if (is9Router) lines.push(windowsHiddenNodeLaunch('%OPENCLAW_HOME%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
4069
+ if (is9Router) lines.push(windowsHiddenNodeLaunch('%DATA_DIR%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
3941
4070
  lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
3942
4071
  lines.push('echo.');
3943
4072
  lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
@@ -3948,12 +4077,182 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3948
4077
  lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
3949
4078
  lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
3950
4079
  }
3951
- lines.push('echo [5/5] Khoi dong gateway multi-bot...');
3952
- lines.push('call openclaw gateway run');
4080
+ const needsZaloLoginMulti = state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal';
4081
+ if (needsZaloLoginMulti) {
4082
+ lines.push('echo [5/6] Khoi dong gateway (cua so moi) de chuan bi dang nhap Zalo...');
4083
+ // Use BAT-native `start` which inherits current env vars - no PS escaping needed
4084
+ lines.push('start "OpenClaw Gateway" cmd /c "openclaw gateway run"');
4085
+ lines.push('echo Cho gateway khoi dong (15 giay)...');
4086
+ lines.push('timeout /t 15 /nobreak >nul');
4087
+ lines.push('echo [6/6] Dang nhap Zalo - dang tao ma QR...');
4088
+ lines.push('openclaw channels login --channel zalouser --instance default --verbose');
4089
+ lines.push('echo.');
4090
+ // Copy QR PNG from TEMP to project dir so user can open it easily
4091
+ lines.push('set "QR_TMP=%TEMP%\\openclaw\\openclaw-zalouser-qr-default.png"');
4092
+ lines.push('if exist "%QR_TMP%" (');
4093
+ lines.push(' copy /y "%QR_TMP%" "%PROJECT_DIR%\\zalo-login-qr.png" >nul');
4094
+ lines.push(' echo ===================================================');
4095
+ lines.push(' echo Ma QR Zalo da duoc luu tai:');
4096
+ lines.push(' echo %PROJECT_DIR%\\zalo-login-qr.png');
4097
+ lines.push(' echo Mo file anh tren r dung Zalo quet de dang nhap!');
4098
+ lines.push(' echo ===================================================');
4099
+ lines.push(' start "" "%PROJECT_DIR%\\zalo-login-qr.png"');
4100
+ lines.push(') else (');
4101
+ lines.push(' echo Khong tim thay file QR. Vui long kiem tra cua so Gateway.');
4102
+ lines.push(')');
4103
+ lines.push('echo Gateway dang chay trong cua so rieng.');
4104
+ lines.push('echo De khoi dong lai: openclaw gateway run');
4105
+ } else {
4106
+ lines.push('echo [5/5] Khoi dong gateway multi-bot...');
4107
+ lines.push(':: Khoi dong OpenClaw Gateway trong cua so moi');
4108
+ lines.push('echo $env:OPENCLAW_HOME = \'%OPENCLAW_HOME%\' > "%TEMP%\\oc-startgw.ps1"');
4109
+ lines.push('echo $env:OPENCLAW_STATE_DIR = \'%OPENCLAW_HOME%\' >> "%TEMP%\\oc-startgw.ps1"');
4110
+ lines.push('echo $b = Join-Path $env:APPDATA \'npm\\openclaw.cmd\' >> "%TEMP%\\oc-startgw.ps1"');
4111
+ lines.push('echo if ^(-not ^(Test-Path $b^)^) { $b = Join-Path $env:APPDATA \'npm\\openclaw\' } >> "%TEMP%\\oc-startgw.ps1"');
4112
+ lines.push("echo Start-Process 'cmd.exe' -WindowStyle Normal -WorkingDirectory '%PROJECT_DIR%' -ArgumentList ^('/c \"' + $b + '\" gateway run'^) >> \"%TEMP%\\oc-startgw.ps1\"");
4113
+ lines.push('powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\\oc-startgw.ps1"');
4114
+ lines.push('del "%TEMP%\\oc-startgw.ps1" >nul 2>&1');
4115
+ lines.push('timeout /t 5 /nobreak >nul');
4116
+ lines.push('echo.');
4117
+ lines.push('echo [OK] OpenClaw Gateway dang khoi dong trong cua so moi!');
4118
+ lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
4119
+ lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
4120
+ }
4121
+ } else if (isComboChannel) {
4122
+ // ── Combo: Telegram + Zalo Personal — 2 bots, 1 gateway ─────────────
4123
+ lines.push('echo [4/5] Tao file cau hinh cho 2 bot (Telegram + Zalo Personal)...');
4124
+ // Bot 0 = Telegram bot
4125
+ const bot0Files = botFiles(0);
4126
+ // Bot 1 = Zalo bot (same workspace root, separate agent/workspace dirs)
4127
+ const bot1 = state.bots[1] || {};
4128
+ const zaloName = bot1.name || 'Zalo Bot';
4129
+ const zaloDesc = bot1.desc || (isVi ? 'Tro ly Zalo ca nhan' : 'Personal Zalo assistant');
4130
+ const zaloPersona = bot1.persona || '';
4131
+ const zaloAgentId = zaloName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'zalo-bot';
4132
+ const bot0Name = (state.bots[0] || {}).name || 'Bot 1';
4133
+ const bot0AgentId = bot0Name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'bot-1';
4134
+ // Merge bot0's config to also include zalo agent in agents.list + bindings
4135
+ const mergedConfig = JSON.parse(bot0Files['./.openclaw/openclaw.json'] || '{}');
4136
+ if (!mergedConfig.agents) mergedConfig.agents = { defaults: {}, list: [] };
4137
+ if (!Array.isArray(mergedConfig.agents.list)) mergedConfig.agents.list = [];
4138
+ // Add Zalo agent to list if not already there
4139
+ const hasZaloInList = mergedConfig.agents.list.some(a => a.id === zaloAgentId);
4140
+ if (!hasZaloInList) {
4141
+ mergedConfig.agents.list.push({
4142
+ id: zaloAgentId,
4143
+ name: zaloName,
4144
+ workspace: `.openclaw/workspace-${zaloAgentId}`,
4145
+ agentDir: `agents/${zaloAgentId}/agent`,
4146
+ model: { primary: (bot1.model || state.config.model) },
4147
+ });
4148
+ }
4149
+ // Ensure bindings exist
4150
+ if (!Array.isArray(mergedConfig.bindings)) mergedConfig.bindings = [];
4151
+ // Bind Telegram bot to bot0
4152
+ const hasTelegramBinding = mergedConfig.bindings.some(b => b && b.match && b.match.channel === 'telegram');
4153
+ if (!hasTelegramBinding) {
4154
+ mergedConfig.bindings.push({ agentId: bot0AgentId, match: { channel: 'telegram', accountId: 'default' } });
4155
+ }
4156
+ // Bind Zalo channel to zalo agent
4157
+ const hasZaloBinding = mergedConfig.bindings.some(b => b && b.match && b.match.channel === 'zalouser');
4158
+ if (!hasZaloBinding) {
4159
+ mergedConfig.bindings.push({ agentId: zaloAgentId, match: { channel: 'zalouser', accountId: 'default' } });
4160
+ }
4161
+ // Ensure zalouser channel is in config
4162
+ if (!mergedConfig.channels) mergedConfig.channels = {};
4163
+ if (!mergedConfig.channels.zalouser) {
4164
+ mergedConfig.channels.zalouser = { enabled: true, dmPolicy: 'open', autoReply: true };
4165
+ }
4166
+ bot0Files['./.openclaw/openclaw.json'] = JSON.stringify(mergedConfig, null, 2);
4167
+ appendBatWriteCommands(lines, mapWindowsNativeFiles(bot0Files));
4168
+ // Zalo agent YAML
4169
+ const zaloAgentYaml = `name: ${zaloAgentId}\ndescription: "${zaloDesc}"\n\nmodel:\n primary: ${bot1.model || state.config.model}`;
4170
+ const zaloWorkspaceDir = `workspace-${zaloAgentId}`;
4171
+ const _zaloSecRules = state.config.securityRules || DEFAULT_SECURITY_RULES[isVi ? 'vi' : 'en'];
4172
+ const zaloFiles = {
4173
+ [`.openclaw/agents/${zaloAgentId}.yaml`]: zaloAgentYaml,
4174
+ [`.openclaw/agents/${zaloAgentId}/agent/auth-profiles.json`]: sharedNativeAuthProfilesContent(),
4175
+ [`.openclaw/${zaloWorkspaceDir}/IDENTITY.md`]: isVi
4176
+ ? `# Danh tinh\n\n- **Ten:** ${zaloName}\n- **Vai tro:** ${zaloDesc}\n\n---\n\nMinh la **${zaloName}**. Khi ai hoi ten, minh tra loi: _"Minh la ${zaloName}"_.`
4177
+ : `# Identity\n\n- **Name:** ${zaloName}\n- **Role:** ${zaloDesc}\n\n---\n\nI am **${zaloName}**. When asked my name, I answer: _"I'm ${zaloName}"_.`,
4178
+ [`.openclaw/${zaloWorkspaceDir}/SOUL.md`]: isVi
4179
+ ? `# Tinh cach\n\n**Huu ich that su.** Bo qua cau ne, cu giup thang.\n**Co ca tinh.** Tro ly khong co ca tinh thi chi la cong cu.\n\n## Phong cach\n- Tu nhien, gan gui\n- Truc tiep, ngan gon${zaloPersona ? `\n\n## Custom Rules\n${zaloPersona}` : ''}`
4180
+ : `# Soul\n\n**Be genuinely helpful.** Skip filler and just help.\n**Have personality.** An assistant with no personality is just a tool.\n\n## Style\n- Natural and concise\n- Direct and practical${zaloPersona ? `\n\n## Custom Rules\n${zaloPersona}` : ''}`,
4181
+ [`.openclaw/${zaloWorkspaceDir}/AGENTS.md`]: isVi
4182
+ ? `# Huong dan van hanh\n\n## Vai tro\nBan la **${zaloName}**, ${zaloDesc.toLowerCase()}.\n\n## Kenh Zalo Personal\n- Ban hoat dong tren kenh Zalo Personal (zca-js).\n- Tra loi moi tin nhan DM theo chinh sach dmPolicy: open.\n- Khong can duoc goi ten moi tra loi (DM la rieng tu).\n\n## Quy tac tra loi\n- Tra loi ngan gon, suc tich\n- Uu tien tieng Viet\n- Khi hoi ten: _"Minh la ${zaloName}"_\n- Khong bia thong tin\n\n${_zaloSecRules}`
4183
+ : `# Operating Manual\n\n## Role\nYou are **${zaloName}**, ${zaloDesc.toLowerCase()}.\n\n## Zalo Personal Channel\n- You operate on the Zalo Personal channel (zca-js).\n- Reply to all DMs with dmPolicy: open.\n- DMs are private — no need to be mentioned to reply.\n\n## Reply Rules\n- Be concise\n- Prefer Vietnamese\n- When asked your name: _"I'm ${zaloName}"_\n- Never fabricate information\n\n${_zaloSecRules}`,
4184
+ [`.openclaw/${zaloWorkspaceDir}/TEAM.md`]: isVi
4185
+ ? `# Doi Bot\n\n## ${bot0Name}\n- Vai tro: ${(state.bots[0] || {}).desc || 'Tro ly Telegram'}\n- Kenh: Telegram\n\n## ${zaloName}\n- Vai tro: ${zaloDesc}\n- Kenh: Zalo Personal`
4186
+ : `# Bot Team\n\n## ${bot0Name}\n- Role: ${(state.bots[0] || {}).desc || 'Telegram assistant'}\n- Channel: Telegram\n\n## ${zaloName}\n- Role: ${zaloDesc}\n- Channel: Zalo Personal`,
4187
+ [`.openclaw/${zaloWorkspaceDir}/USER.md`]: isVi
4188
+ ? `# Thong tin nguoi dung\n\n## Tong quan\n- **Ngon ngu uu tien:** Tieng Viet\n\n## Thong tin ca nhan\n${state.config.userInfo || '- _(Chua co gi)_'}`
4189
+ : `# User Profile\n\n## Overview\n- **Preferred language:** Vietnamese\n\n## Notes\n${state.config.userInfo || '- _(Nothing yet)_'}`,
4190
+ [`.openclaw/${zaloWorkspaceDir}/MEMORY.md`]: isVi
4191
+ ? `# Bo nho dai han\n\n## Ghi chu\n- _(Chua co gi)_`
4192
+ [`.openclaw/${zaloWorkspaceDir}/TOOLS.md`]: isVi
4193
+ ? `# Hướng dẫn sử dụng Tools\n\n## Skills đã cài\n${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(Chưa có skill nào)_'}\n\n## Quy ước\n- Ưu tiên dùng tool thay vì đoán\n- Browser: dùng khi user yêu cầu thao tác web\n- Memory: cập nhật khi biết thông tin quan trọng\n\n## Ghi chú thiết lập của bạn\n\nGhi lại cấu hình riêng của môi trường bạn, ví dụ:\n- Tên thiết bị, camera, SSH hosts\n- Giọng nói ưa thích (TTS)\n\n---\n\nThêm ghi chú nào giúp ích cho công việc của bạn.`
4194
+ : `# Tool Usage Guide\n\n## Installed Skills\n${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills installed)_'}\n\n## Conventions\n- Prefer tools over guessing\n- Use Browser for explicit web tasks\n- Update Memory when important user info appears\n\n## Your Setup Notes\n\nRecord environment-specific config, e.g.:\n- Device names, cameras, SSH hosts\n- Preferred TTS voice\n\n---\n\nAdd whatever helps you do your job.`,
4195
+ : `# Long-term Memory\n\n## Notes\n- _(Nothing yet)_`,
4196
+ };
4197
+ appendBatWriteCommands(lines, mapWindowsNativeFiles(zaloFiles));
4198
+ if (is9Router) lines.push(windowsHiddenNodeLaunch('%DATA_DIR%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
4199
+ lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
4200
+ lines.push('echo.');
4201
+ lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
4202
+ lines.push('echo Other reachable URLs: http://localhost:18791');
4203
+ lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
4204
+ if (is9Router) {
4205
+ lines.push('echo.');
4206
+ lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
4207
+ lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
4208
+ }
4209
+ // Login Zalo trực tiếp (không cần gateway chạy trước — openclaw channels login standalone)
4210
+ // Sau khi login thành công, gateway sẽ tự nhận credentials khi khởi động.
4211
+ lines.push('echo [5/6] Dang nhap Zalo Personal...');
4212
+ lines.push('echo.');
4213
+ lines.push('echo === HUONG DAN DANG NHAP ZALO ===');
4214
+ lines.push('echo OpenClaw se hien duong dan file anh QR trong cua so nay.');
4215
+ lines.push('echo Hay mo file anh do, chon Quet QR trong app Zalo va quet.');
4216
+ lines.push('echo Neu het gio, nhap R de tao ma QR moi.');
4217
+ lines.push('echo ================================');
4218
+ lines.push('echo.');
4219
+ lines.push(':retry_zalo');
4220
+ lines.push('echo Dang tao ma QR Zalo moi...');
4221
+ lines.push('echo.');
4222
+ lines.push('call openclaw channels login --channel zalouser --verbose');
4223
+ lines.push('if %ERRORLEVEL% equ 0 goto :zalo_done');
4224
+ lines.push('echo.');
4225
+ lines.push('echo [WARN] Ma QR het han hoac chua duoc quet kip.');
4226
+ lines.push('echo R - Tao ma QR moi va thu lai');
4227
+ lines.push('echo Enter - Bo qua dang nhap Zalo (Zalo se khong hoat dong)');
4228
+ lines.push('set /p RETRY_ZALO=Ban chon: ');
4229
+ lines.push('if /i "%RETRY_ZALO%"=="R" goto :retry_zalo');
4230
+ lines.push('echo [SKIP] Bo qua. Zalo se khong hoat dong cho den khi dang nhap lai.');
4231
+ lines.push('goto :zalo_continue');
4232
+ lines.push(':zalo_done');
4233
+ lines.push('echo [OK] Dang nhap Zalo thanh cong!');
4234
+ lines.push(':zalo_continue');
4235
+ lines.push(':: Dong gateway cu (neu co lock file) truoc khi khoi dong lai');
4236
+ lines.push('call openclaw gateway stop 2>nul');
4237
+ lines.push('timeout /t 2 /nobreak >nul');
4238
+ lines.push('echo [6/6] Khoi dong bot (Telegram + Zalo)...');
4239
+ lines.push(':: Khoi dong OpenClaw Gateway trong cua so moi');
4240
+ lines.push('echo $env:OPENCLAW_HOME = \'%OPENCLAW_HOME%\' > "%TEMP%\\oc-startgw.ps1"');
4241
+ lines.push('echo $env:OPENCLAW_STATE_DIR = \'%OPENCLAW_HOME%\' >> "%TEMP%\\oc-startgw.ps1"');
4242
+ lines.push('echo $b = Join-Path $env:APPDATA \'npm\\openclaw.cmd\' >> "%TEMP%\\oc-startgw.ps1"');
4243
+ lines.push('echo if ^(-not ^(Test-Path $b^)^) { $b = Join-Path $env:APPDATA \'npm\\openclaw\' } >> "%TEMP%\\oc-startgw.ps1"');
4244
+ lines.push("echo Start-Process 'cmd.exe' -WindowStyle Normal -WorkingDirectory '%PROJECT_DIR%' -ArgumentList ^('/c \"' + $b + '\" gateway run'^) >> \"%TEMP%\\oc-startgw.ps1\"");
4245
+ lines.push('powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\\oc-startgw.ps1"');
4246
+ lines.push('del "%TEMP%\\oc-startgw.ps1" >nul 2>&1');
4247
+ lines.push('timeout /t 5 /nobreak >nul');
4248
+ lines.push('echo.');
4249
+ lines.push('echo [OK] OpenClaw Gateway dang khoi dong trong cua so moi!');
4250
+ lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
4251
+ lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
3953
4252
  } else {
3954
4253
  lines.push('echo [4/5] Tao file cau hinh...');
3955
4254
  appendBatWriteCommands(lines, mapWindowsNativeFiles(botFiles(0)));
3956
- if (is9Router) lines.push(windowsHiddenNodeLaunch('%OPENCLAW_HOME%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
4255
+ if (is9Router) lines.push(windowsHiddenNodeLaunch('%DATA_DIR%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
3957
4256
  lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
3958
4257
  lines.push('echo.');
3959
4258
  lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
@@ -3964,8 +4263,55 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
3964
4263
  lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
3965
4264
  lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
3966
4265
  }
3967
- lines.push('echo [5/5] Khoi dong bot...');
3968
- lines.push('call openclaw gateway run');
4266
+ const needsZaloLogin = state.channel === 'zalo-personal';
4267
+ if (needsZaloLogin) {
4268
+ // Login Zalo trực tiếp (không cần gateway chạy trước)
4269
+ lines.push('echo [5/5] Dang nhap Zalo Personal...');
4270
+ lines.push('echo.');
4271
+ lines.push('echo === HUONG DAN DANG NHAP ZALO ===');
4272
+ lines.push('echo Cua so Zalo Login se mo. Hay:');
4273
+ lines.push('echo 1. Doi QR hien ra trong cua so Zalo Login');
4274
+ lines.push('echo 2. Mo app Zalo, chon Quet QR va quet ma');
4275
+ lines.push('echo 3. Doi thay chu "Login successful" trong cua so do');
4276
+ lines.push('echo 4. Dong cua so Zalo Login');
4277
+ lines.push('echo ================================');
4278
+ lines.push('echo.');
4279
+ 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"');
4280
+ lines.push('echo Nhan phim bat ky sau khi dong cua so Zalo Login...');
4281
+ lines.push('pause >nul');
4282
+ lines.push(':: Dong gateway cu (neu co lock file tu cua so Zalo Login) truoc khi khoi dong lai');
4283
+ lines.push('call openclaw gateway stop 2>nul');
4284
+ lines.push('timeout /t 2 /nobreak >nul');
4285
+ lines.push('echo [6/6] Khoi dong bot...');
4286
+ lines.push(':: Khoi dong OpenClaw Gateway trong cua so moi');
4287
+ lines.push('echo $env:OPENCLAW_HOME = \'%OPENCLAW_HOME%\' > "%TEMP%\\oc-startgw.ps1"');
4288
+ lines.push('echo $env:OPENCLAW_STATE_DIR = \'%OPENCLAW_HOME%\' >> "%TEMP%\\oc-startgw.ps1"');
4289
+ lines.push('echo $b = Join-Path $env:APPDATA \'npm\\openclaw.cmd\' >> "%TEMP%\\oc-startgw.ps1"');
4290
+ lines.push('echo if ^(-not ^(Test-Path $b^)^) { $b = Join-Path $env:APPDATA \'npm\\openclaw\' } >> "%TEMP%\\oc-startgw.ps1"');
4291
+ lines.push("echo Start-Process 'cmd.exe' -WindowStyle Normal -WorkingDirectory '%PROJECT_DIR%' -ArgumentList ^('/c \"' + $b + '\" gateway run'^) >> \"%TEMP%\\oc-startgw.ps1\"");
4292
+ lines.push('powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\\oc-startgw.ps1"');
4293
+ lines.push('del "%TEMP%\\oc-startgw.ps1" >nul 2>&1');
4294
+ lines.push('timeout /t 5 /nobreak >nul');
4295
+ lines.push('echo.');
4296
+ lines.push('echo [OK] OpenClaw Gateway dang khoi dong trong cua so moi!');
4297
+ lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
4298
+ lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
4299
+ } else {
4300
+ lines.push('echo [5/5] Khoi dong bot...');
4301
+ lines.push(':: Khoi dong OpenClaw Gateway trong cua so moi');
4302
+ lines.push('echo $env:OPENCLAW_HOME = \'%OPENCLAW_HOME%\' > "%TEMP%\\oc-startgw.ps1"');
4303
+ lines.push('echo $env:OPENCLAW_STATE_DIR = \'%OPENCLAW_HOME%\' >> "%TEMP%\\oc-startgw.ps1"');
4304
+ lines.push('echo $b = Join-Path $env:APPDATA \'npm\\openclaw.cmd\' >> "%TEMP%\\oc-startgw.ps1"');
4305
+ lines.push('echo if ^(-not ^(Test-Path $b^)^) { $b = Join-Path $env:APPDATA \'npm\\openclaw\' } >> "%TEMP%\\oc-startgw.ps1"');
4306
+ lines.push("echo Start-Process 'cmd.exe' -WindowStyle Normal -WorkingDirectory '%PROJECT_DIR%' -ArgumentList ^('/c \"' + $b + '\" gateway run'^) >> \"%TEMP%\\oc-startgw.ps1\"");
4307
+ lines.push('powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\\oc-startgw.ps1"');
4308
+ lines.push('del "%TEMP%\\oc-startgw.ps1" >nul 2>&1');
4309
+ lines.push('timeout /t 5 /nobreak >nul');
4310
+ lines.push('echo.');
4311
+ lines.push('echo [OK] OpenClaw Gateway dang khoi dong trong cua so moi!');
4312
+ lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
4313
+ lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
4314
+ }
3969
4315
  }
3970
4316
 
3971
4317
  lines.push('goto :end');
@@ -4073,7 +4419,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
4073
4419
  if (is9Router) {
4074
4420
  vps.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
4075
4421
  vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$NINE_ROUTER_ENTRY" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
4076
- vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
4422
+ vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.9router/9router-smart-route-sync.js"');
4077
4423
  }
4078
4424
  vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
4079
4425
  vps.push('pm2 save && pm2 startup');
@@ -4087,7 +4433,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
4087
4433
  if (is9Router) {
4088
4434
  vps.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
4089
4435
  vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$NINE_ROUTER_ENTRY" --name openclaw-9router --interpreter "$(command -v node)"');
4090
- vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
4436
+ vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.9router/9router-smart-route-sync.js"');
4091
4437
  }
4092
4438
  vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
4093
4439
  vps.push('pm2 save && pm2 startup');
@@ -4175,17 +4521,294 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
4175
4521
 
4176
4522
 
4177
4523
 
4524
+ // ========== Generate Uninstall Script ==========
4525
+ function generateUninstallScript() {
4526
+ const os = state.nativeOs || 'win';
4527
+ const isDocker = state.deployMode === 'docker';
4528
+ const projectDirRaw = document.getElementById('cfg-project-path')?.value?.trim() || '.';
4529
+ // Normalise to a sensible display path
4530
+ const projectDir = projectDirRaw;
4531
+ const absWin = projectDir.replace(/\//g, '\\');
4532
+ const absUnix = projectDir.replace(/\\/g, '/');
4533
+ const botName = (state.bots[0]?.name || 'openclaw').toLowerCase().replace(/[^a-z0-9]+/g, '-');
4534
+
4535
+ // ── Windows native .bat ────────────────────────────────────────────────────
4536
+ if (os === 'win' && !isDocker) {
4537
+ return {
4538
+ name: 'uninstall-openclaw-win.bat',
4539
+ content: `@echo off
4540
+ setlocal EnableExtensions
4541
+ chcp 65001 >nul
4542
+ echo.
4543
+ echo ============================================================
4544
+ echo OpenClaw Uninstaller - Windows Native
4545
+ echo Project: ${absWin}
4546
+ echo ============================================================
4547
+ echo.
4548
+ echo [WARNING] This will:
4549
+ echo 1. Kill openclaw and 9router background processes
4550
+ echo 2. Uninstall global npm packages (openclaw, 9router)
4551
+ echo 3. Delete the project folder and all its data
4552
+ echo.
4553
+ set /p CONFIRM=Nhap YES de xac nhan xoa toan bo:
4554
+ if /i not "%CONFIRM%"=="YES" (
4555
+ echo Huy bo. Khong xoa gi ca.
4556
+ pause
4557
+ exit /b 0
4558
+ )
4559
+ echo.
4560
+ echo [1/4] Dang dung cac tien trinh openclaw va 9router...
4561
+ wmic process where "Name='node.exe' and CommandLine like '%%9router%%'" delete >nul 2>&1
4562
+ wmic process where "Name='cmd.exe' and CommandLine like '%%9router%%'" delete >nul 2>&1
4563
+ wmic process where "Name='node.exe' and CommandLine like '%%openclaw.mjs%%'" delete >nul 2>&1
4564
+ timeout /t 2 /nobreak >nul
4565
+ echo OK: Tien trinh da dung.
4566
+ echo.
4567
+ echo [2/4] Dang go cai npm packages toan cau...
4568
+ set "PATH=%APPDATA%\\npm;%PATH%"
4569
+ call npm uninstall -g openclaw 9router grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>nul
4570
+ echo OK: npm packages da duoc go cai.
4571
+ echo.
4572
+ echo [3/4] Xoa thu muc project...
4573
+ set "TARGET=${absWin}"
4574
+ if exist "%TARGET%" (
4575
+ rd /s /q "%TARGET%"
4576
+ echo OK: Da xoa %TARGET%
4577
+ ) else (
4578
+ echo INFO: Thu muc khong ton tai: %TARGET%
4579
+ )
4580
+ echo.
4581
+ echo [4/4] Xoa thu muc .9router trong Home (neu co)...
4582
+ if exist "%USERPROFILE%\\.9router" (
4583
+ set /p CLEAN_HOME=Xoa ca %USERPROFILE%\\.9router? [YES/no]:
4584
+ if /i "%CLEAN_HOME%"=="YES" rd /s /q "%USERPROFILE%\\.9router" >nul 2>&1
4585
+ )
4586
+ echo.
4587
+ echo ============================================================
4588
+ echo Go cai hoan tat!
4589
+ echo De cai lai: chay lai file setup hoac npx create-openclaw-bot
4590
+ echo ============================================================
4591
+ pause
4592
+ endlocal
4593
+ `
4594
+ };
4595
+ }
4596
+
4597
+ // ── Windows Docker .bat ────────────────────────────────────────────────────
4598
+ if (os === 'win' && isDocker) {
4599
+ return {
4600
+ name: 'uninstall-openclaw-docker.bat',
4601
+ content: `@echo off
4602
+ setlocal EnableExtensions
4603
+ chcp 65001 >nul
4604
+ echo.
4605
+ echo ============================================================
4606
+ echo OpenClaw Uninstaller - Docker (Windows)
4607
+ echo Project: ${absWin}
4608
+ echo ============================================================
4609
+ echo.
4610
+ echo [WARNING] This will stop Docker containers and delete the project folder.
4611
+ echo.
4612
+ set /p CONFIRM=Nhap YES de xac nhan xoa toan bo:
4613
+ if /i not "%CONFIRM%"=="YES" (
4614
+ echo Huy bo. Khong xoa gi ca.
4615
+ pause
4616
+ exit /b 0
4617
+ )
4618
+ echo.
4619
+ echo [1/2] Dang dung Docker containers...
4620
+ cd /d "${absWin}\docker\openclaw" 2>nul && (
4621
+ docker compose down --volumes --remove-orphans 2>nul || docker-compose down --volumes --remove-orphans 2>nul
4622
+ echo OK: Containers da dung.
4623
+ ) || echo INFO: Khong tim thay docker compose.
4624
+ echo.
4625
+ echo [2/2] Xoa thu muc project...
4626
+ cd /d "%USERPROFILE%"
4627
+ if exist "${absWin}" (
4628
+ rd /s /q "${absWin}"
4629
+ echo OK: Da xoa ${absWin}
4630
+ )
4631
+ echo.
4632
+ echo ============================================================
4633
+ echo Go cai hoan tat! De cai lai: npx create-openclaw-bot@latest
4634
+ echo ============================================================
4635
+ pause
4636
+ endlocal
4637
+ `
4638
+ };
4639
+ }
4640
+
4641
+ // ── VPS / PM2 .sh ─────────────────────────────────────────────────────────
4642
+ if (os === 'vps') {
4643
+ return {
4644
+ name: 'uninstall-openclaw-vps.sh',
4645
+ content: `#!/usr/bin/env bash
4646
+ # ====== OpenClaw Uninstaller — VPS / Ubuntu Server (PM2) ======
4647
+ set -e
4648
+ PROJECT_DIR="${absUnix}"
4649
+ APP_NAME="${botName}"
4650
+
4651
+ echo ""
4652
+ echo "============================================================"
4653
+ echo " OpenClaw Uninstaller — VPS / Ubuntu Server"
4654
+ echo " Project: $PROJECT_DIR"
4655
+ echo " PM2 app: $APP_NAME"
4656
+ echo "============================================================"
4657
+ echo ""
4658
+ read -rp "Type YES to confirm full removal: " CONFIRM
4659
+ if [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi
4660
+
4661
+ echo "[1/5] Stopping PM2 processes..."
4662
+ if command -v pm2 &>/dev/null; then
4663
+ pm2 delete "$APP_NAME" "$APP_NAME-9router" "$APP_NAME-9router-sync" openclaw openclaw-multibot 2>/dev/null || true
4664
+ pm2 save --force 2>/dev/null || true
4665
+ fi
4666
+
4667
+ echo "[2/5] Killing leftover processes on ports 18791 / 20128..."
4668
+ for port in 18791 20128; do
4669
+ pid=$(lsof -ti tcp:\$port 2>/dev/null || true)
4670
+ [ -n "\$pid" ] && kill -9 \$pid 2>/dev/null || true
4671
+ done
4672
+
4673
+ echo "[3/5] Uninstalling npm packages..."
4674
+ npm uninstall -g openclaw 9router pm2 grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>/dev/null || true
4675
+
4676
+ echo "[4/5] Removing project directory..."
4677
+ [ -d "\$PROJECT_DIR" ] && rm -rf "\$PROJECT_DIR" && echo " OK: Deleted \$PROJECT_DIR" || echo " INFO: Not found."
4678
+
4679
+ echo "[5/5] Checking home-level .9router / .openclaw..."
4680
+ for dir in "\$HOME/.9router" "\$HOME/.openclaw"; do
4681
+ if [ -d "\$dir" ]; then
4682
+ read -rp "Delete \$dir ? [YES/no]: " CLEAN
4683
+ [ "\$CLEAN" = "YES" ] && rm -rf "\$dir" && echo " OK: Deleted \$dir" || echo " Kept: \$dir"
4684
+ fi
4685
+ done
4686
+
4687
+ echo ""
4688
+ echo "============================================================"
4689
+ echo " Uninstall complete! Re-install: npx create-openclaw-bot@latest"
4690
+ echo "============================================================"
4691
+ `
4692
+ };
4693
+ }
4694
+
4695
+ // ── macOS / Linux Desktop .sh ──────────────────────────────────────────────
4696
+ if (os === 'linux' || os === 'linux-desktop') {
4697
+ const label = os === 'linux' ? 'macOS' : 'Linux Desktop';
4698
+ return {
4699
+ name: 'uninstall-openclaw.sh',
4700
+ content: `#!/usr/bin/env bash
4701
+ # ====== OpenClaw Uninstaller — ${label} (Native) ======
4702
+ set -e
4703
+ PROJECT_DIR="${absUnix}"
4704
+
4705
+ echo ""
4706
+ echo "============================================================"
4707
+ echo " OpenClaw Uninstaller — ${label} Native"
4708
+ echo " Project: $PROJECT_DIR"
4709
+ echo "============================================================"
4710
+ echo ""
4711
+ read -rp "Type YES to confirm full removal: " CONFIRM
4712
+ if [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi
4713
+
4714
+ echo "[1/4] Stopping openclaw and 9router processes..."
4715
+ pkill -f "openclaw gateway run" 2>/dev/null || true
4716
+ pkill -f "9router.*20128" 2>/dev/null || true
4717
+ pkill -f "9router-smart-route" 2>/dev/null || true
4718
+ pkill -f "\$PROJECT_DIR" 2>/dev/null || true
4719
+ for port in 18791 20128; do
4720
+ pid=$(lsof -ti tcp:\$port 2>/dev/null || true)
4721
+ [ -n "\$pid" ] && kill -9 \$pid 2>/dev/null || true
4722
+ done
4723
+
4724
+ echo "[2/4] Uninstalling npm packages..."
4725
+ npm uninstall -g openclaw 9router grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>/dev/null || true
4726
+ sudo npm uninstall -g openclaw 9router 2>/dev/null || true
4727
+
4728
+ echo "[3/4] Removing project directory..."
4729
+ [ -d "\$PROJECT_DIR" ] && rm -rf "\$PROJECT_DIR" && echo " OK: Deleted \$PROJECT_DIR" || echo " INFO: Not found."
4730
+
4731
+ echo "[4/4] Checking home-level .9router / .openclaw..."
4732
+ for dir in "\$HOME/.9router" "\$HOME/.openclaw"; do
4733
+ if [ -d "\$dir" ]; then
4734
+ read -rp "Delete \$dir ? [YES/no]: " CLEAN
4735
+ [ "\$CLEAN" = "YES" ] && rm -rf "\$dir" && echo " OK: Deleted \$dir" || echo " Kept: \$dir"
4736
+ fi
4737
+ done
4738
+
4739
+ echo ""
4740
+ echo "============================================================"
4741
+ echo " Uninstall complete! Re-install: run setup script or npx create-openclaw-bot"
4742
+ echo "============================================================"
4743
+ `
4744
+ };
4745
+ }
4746
+
4747
+ // ── Docker macOS/Linux/VPS .sh ─────────────────────────────────────────────
4748
+ if (isDocker) {
4749
+ return {
4750
+ name: 'uninstall-openclaw-docker.sh',
4751
+ content: `#!/usr/bin/env bash
4752
+ # ====== OpenClaw Uninstaller — Docker ======
4753
+ set -e
4754
+ PROJECT_DIR="${absUnix}"
4755
+ DOCKER_DIR="$PROJECT_DIR/docker/openclaw"
4756
+
4757
+ echo ""
4758
+ echo "============================================================"
4759
+ echo " OpenClaw Uninstaller — Docker"
4760
+ echo " Project: $PROJECT_DIR"
4761
+ echo "============================================================"
4762
+ echo ""
4763
+ read -rp "Type YES to confirm full removal: " CONFIRM
4764
+ if [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi
4765
+
4766
+ echo "[1/3] Stopping Docker containers and removing volumes..."
4767
+ if [ -d "\$DOCKER_DIR" ] && command -v docker &>/dev/null; then
4768
+ cd "\$DOCKER_DIR"
4769
+ docker compose down --volumes --remove-orphans 2>/dev/null || docker-compose down --volumes --remove-orphans 2>/dev/null || true
4770
+ fi
4771
+
4772
+ echo "[2/3] Removing project directory..."
4773
+ [ -d "\$PROJECT_DIR" ] && rm -rf "\$PROJECT_DIR" && echo " OK: Deleted \$PROJECT_DIR" || echo " INFO: Not found."
4774
+
4775
+ echo "[3/3] Checking home-level .openclaw..."
4776
+ if [ -d "\$HOME/.openclaw" ]; then
4777
+ read -rp "Delete \$HOME/.openclaw? [YES/no]: " CLEAN
4778
+ [ "\$CLEAN" = "YES" ] && rm -rf "\$HOME/.openclaw" && echo " OK." || echo " Kept."
4779
+ fi
4780
+
4781
+ echo ""
4782
+ echo "============================================================"
4783
+ echo " Uninstall complete! Re-install: npx create-openclaw-bot@latest"
4784
+ echo "============================================================"
4785
+ `
4786
+ };
4787
+ }
4788
+
4789
+ return null;
4790
+ }
4791
+
4792
+ function _triggerDownload(filename, content, mimeType) {
4793
+ const blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' });
4794
+ const url = URL.createObjectURL(blob);
4795
+ const a = document.createElement('a');
4796
+ a.href = url; a.download = filename; a.style.display = 'none';
4797
+ document.body.appendChild(a); a.click();
4798
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 1500);
4799
+ }
4800
+
4178
4801
  window.downloadNativeScript = function() {
4179
4802
  // Regenerate output first so the downloaded script always matches the latest wizard state.
4180
4803
  generateOutput();
4181
4804
  const script = window._nativeScript;
4182
4805
  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);
4806
+ _triggerDownload(script.name, script.content, 'text/plain;charset=utf-8');
4807
+ // Also download the matching uninstall script right after
4808
+ const uninstall = generateUninstallScript();
4809
+ if (uninstall) {
4810
+ setTimeout(() => _triggerDownload(uninstall.name, uninstall.content, 'text/plain;charset=utf-8'), 600);
4811
+ }
4189
4812
  };
4190
4813
 
4191
4814
  // ========== Generate Windows Auto Setup .bat ==========
@@ -4244,10 +4867,23 @@ New-Item -ItemType Directory -Force -Path "$projectDir" | Out-Null
4244
4867
  // [3/4] Docker build
4245
4868
  ps += `# [3/4] Docker build
4246
4869
  Write-Host "[3/4] ${isVi ? 'Build Docker image (co the mat vai phut)...' : 'Building Docker image (may take a few minutes)...'}" -ForegroundColor Yellow
4870
+ \$dockerExe = \$null
4871
+ try { \$dockerExe = (Get-Command 'docker' -ErrorAction Stop).Source } catch {}
4872
+ if (-not \$dockerExe) {
4873
+ 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
4874
+ Read-Host "Press Enter to exit"
4875
+ exit 1
4876
+ }
4877
+ & \$dockerExe info 2>&1 | Out-Null
4878
+ if (\$LASTEXITCODE -ne 0) {
4879
+ 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
4880
+ Read-Host "Press Enter to exit"
4881
+ exit 1
4882
+ }
4247
4883
  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
4884
+ & \$dockerExe compose build
4885
+ if (\$LASTEXITCODE -ne 0) {
4886
+ Write-Host " ❌ ${isVi ? 'Docker build that bai. Xem log phia tren.' : 'Docker build failed. Check the log above.'}" -ForegroundColor Red
4251
4887
  Read-Host "${isVi ? 'Nhan Enter de thoat' : 'Press Enter to exit'}"
4252
4888
  exit 1
4253
4889
  }
@@ -4258,7 +4894,7 @@ Write-Host " ✅ ${isVi ? 'Docker image da build' : 'Docker image built'}" -For
4258
4894
  // [4/4] Docker up
4259
4895
  ps += `# [4/4] Start bot
4260
4896
  Write-Host "[4/4] ${isVi ? 'Khoi dong bot...' : 'Starting bot...'}" -ForegroundColor Yellow
4261
- docker compose up -d
4897
+ & \$dockerExe compose up -d
4262
4898
  Write-Host " ✅ ${isVi ? 'Bot dang chay!' : 'Bot is running!'}" -ForegroundColor Green
4263
4899
 
4264
4900
  Write-Host ""
@@ -4270,8 +4906,8 @@ Write-Host " 🎉 ${isVi ? 'Setup hoan tat!' : 'Setup complete!'}" -ForegroundC
4270
4906
  if (is9Router) {
4271
4907
  ps += `Write-Host " ${isVi ? 'Mo http://localhost:30128/dashboard de login OAuth' : 'Open http://localhost:30128/dashboard to login OAuth'}" -ForegroundColor White\n`;
4272
4908
  }
4273
- if (state.channel === 'zalo-personal') {
4274
- 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`;
4909
+ if (state.channel === 'zalo-personal' || state.channel === 'telegram+zalo-personal') {
4910
+ ps += `Write-Host " ${isVi ? 'Chay: docker compose exec -it ai-bot openclaw channels login --channel zalouser --instance default --verbose' : 'Run: docker compose exec -it ai-bot openclaw channels login --channel zalouser --instance default --verbose'}" -ForegroundColor White\n`;
4275
4911
  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
4912
  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`;
4277
4913
  }
@@ -4452,7 +5088,7 @@ echo ""
4452
5088
 
4453
5089
  function generateZaloOnboardGuide() {
4454
5090
  const lang = document.getElementById('cfg-language')?.value || 'vi';
4455
- setOutput('out-zalo-onboard-cmd', `docker compose exec -it ai-bot openclaw channels login --channel zalouser --verbose`);
5091
+ setOutput('out-zalo-onboard-cmd', `docker compose exec -it ai-bot openclaw channels login --channel zalouser --instance default --verbose`);
4456
5092
 
4457
5093
  if (lang === 'vi') {
4458
5094
  setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
@@ -4524,6 +5160,18 @@ echo ""
4524
5160
  zip.file(path, content);
4525
5161
  });
4526
5162
 
5163
+ // Include the native setup script (if native mode)
5164
+ const nativeScript = window._nativeScript;
5165
+ if (nativeScript && nativeScript.name && nativeScript.content) {
5166
+ zip.file(nativeScript.name, nativeScript.content);
5167
+ }
5168
+
5169
+ // Include the matching uninstall script
5170
+ const uninstall = generateUninstallScript();
5171
+ if (uninstall && uninstall.name && uninstall.content) {
5172
+ zip.file(uninstall.name, uninstall.content);
5173
+ }
5174
+
4527
5175
  const blob = await zip.generateAsync({ type: 'blob' });
4528
5176
  const url = URL.createObjectURL(blob);
4529
5177
  const a = document.createElement('a');