create-openclaw-bot 4.1.4 → 5.0.2

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
@@ -26,8 +26,10 @@
26
26
  // ========== State ==========
27
27
  const state = {
28
28
  currentStep: 1,
29
- totalSteps: 4,
29
+ totalSteps: 5,
30
30
  channel: null,
31
+ deployMode: 'docker', // 'docker' | 'native'
32
+ nativeOs: 'win', // 'win' | 'linux' | 'vps' | 'linux-desktop'
31
33
  config: {
32
34
  botName: '',
33
35
  description: '',
@@ -40,9 +42,14 @@
40
42
  securityRules: '',
41
43
  plugins: [],
42
44
  skills: [],
45
+ // Persisted credential inputs (Bug 1+2 fix)
46
+ botToken: '',
47
+ apiKey: '',
48
+ projectPath: '',
43
49
  },
44
50
  };
45
51
 
52
+
46
53
  // ========== AI Providers & Models ==========
47
54
  const PROVIDERS = {
48
55
  google: {
@@ -56,7 +63,7 @@
56
63
  models: [
57
64
  { id: 'google/gemini-2.5-flash', name: 'Gemini 2.5 Flash', descVi: 'Nhanh, miễn phí, đa năng', descEn: 'Fast, free, versatile', badge: '🆓 Free' },
58
65
  { id: 'google/gemini-2.5-pro', name: 'Gemini 2.5 Pro', descVi: 'Thông minh hơn, phân tích sâu', descEn: 'Smarter, deeper analysis', badge: '🆓 Free' },
59
- { id: 'google/gemini-3.0-flash', name: 'Gemini 3.0 Flash', descVi: 'Thế hệ mới, cực nhanh', descEn: 'Next gen, extremely fast', badge: '🆓 Free' },
66
+ { id: 'google/gemini-3-flash', name: 'Gemini 3 Flash', descVi: 'Thế hệ mới, cực nhanh', descEn: 'Next gen, extremely fast', badge: '🆓 Free' },
60
67
  ],
61
68
  },
62
69
  anthropic: {
@@ -112,6 +119,10 @@
112
119
  free: true,
113
120
  isLocal: true,
114
121
  models: [
122
+ { id: 'ollama/gemma4:e2b', name: 'Gemma 4 E2B', descVi: '🟢 Nhẹ nhất (~4-6 GB RAM) — Edge, laptop, test nhanh', descEn: '🟢 Lightest (~4-6 GB RAM) — Edge, laptop, fastest startup', badge: '🆕 Apr 2 2026' },
123
+ { id: 'ollama/gemma4:e4b', name: 'Gemma 4 E4B', descVi: '🟡 Cân bằng (~8-10 GB RAM) — Khuyên dùng', descEn: '🟡 Balanced (~8-10 GB RAM) — Recommended', badge: '🆕 Apr 2 2026' },
124
+ { id: 'ollama/gemma4:26b', name: 'Gemma 4 26B', descVi: '🟠 Mạnh (~18-24 GB RAM/VRAM) — Máy mạnh', descEn: '🟠 Powerful (~18-24 GB RAM/VRAM) — High-end machine', badge: '🆕 Apr 2 2026' },
125
+ { id: 'ollama/gemma4:31b', name: 'Gemma 4 31B', descVi: '🔴 Mạnh nhất (~24+ GB RAM/VRAM) — Workstation/GPU', descEn: '🔴 Most powerful (~24+ GB RAM/VRAM) — Workstation/GPU', badge: '🆕 Apr 2 2026' },
115
126
  { id: 'ollama/qwen3:8b', name: 'Qwen 3 8B', descVi: 'Đa ngôn ngữ, nhẹ', descEn: 'Multi-lingual, lightweight', badge: '🏠 Local' },
116
127
  { id: 'ollama/deepseek-r1:8b', name: 'DeepSeek R1 8B', descVi: 'Suy luận, code', descEn: 'Reasoning, code', badge: '🏠 Local' },
117
128
  { id: 'ollama/llama3.3:8b', name: 'Llama 3.3 8B', descVi: 'Meta, đa năng', descEn: 'Meta, versatile', badge: '🏠 Local' },
@@ -125,7 +136,7 @@
125
136
  envKey: null,
126
137
  envLabel: null,
127
138
  envLink: 'https://github.com/decolua/9router',
128
- envInstructionsVi: '9Router chạy cùng Docker — <strong>không cần API key</strong>. Sau khi <code>docker compose up</code>, mở <a href="http://localhost:20128/dashboard" target="_blank">localhost:20128/dashboard</a> → đăng nhập OAuth.<br><span style="color:var(--danger)">⚠️ <b>CẢNH BÁO:</b> TUYỆT ĐỐI KHÔNG dùng Provider Antigravity (nguy cơ bị ban Google Account vĩnh viễn).</span>', envInstructionsEn: '9Router runs with Docker — <strong>no API key needed</strong>. After <code>docker compose up</code>, open <a href="http://localhost:20128/dashboard" target="_blank">localhost:20128/dashboard</a> and OAuth login.<br><span style="color:var(--danger)">⚠️ <b>WARNING:</b> DO NOT use Antigravity as an OAuth Provider (high risk of permanent Google Account ban).</span>',
139
+ envInstructionsVi: '9Router chạy cùng Docker — <strong>không cần API key</strong>. Sau khi <code>docker compose up</code>, mở <a href="http://localhost:20128/dashboard" target="_blank">localhost:20128/dashboard</a> → đăng nhập OAuth.<br>✅ <b>Mới v0.3.75:</b> Claude Code, Codex, Gemini CLI và Antigravity có thể dùng 9Router làm endpoint trực tiếp.<br><span style="color:var(--danger)">⚠️ <b>CẢNH BÁO:</b> TUYỆT ĐỐI KHÔNG chọn Provider <b>Antigravity</b> khi đăng nhập OAuth trên dashboard 9Router (nguy cơ bị ban Google Account vĩnh viễn).</span>', envInstructionsEn: '9Router runs with Docker — <strong>no API key needed</strong>. After <code>docker compose up</code>, open <a href="http://localhost:20128/dashboard" target="_blank">localhost:20128/dashboard</a> and OAuth login.<br>✅ <b>New in v0.3.75:</b> Claude Code, Codex, Gemini CLI, and Antigravity can use 9Router as their endpoint directly.<br><span style="color:var(--danger)">⚠️ <b>WARNING:</b> Do NOT select <b>Antigravity</b> as your OAuth Provider when logging into the 9Router dashboard (high risk of permanent Google Account ban).</span>',
129
140
  free: true,
130
141
  isProxy: true,
131
142
  models: [
@@ -168,18 +179,10 @@
168
179
 
169
180
  // ========== Available Skills (ClawHub registry — agent capabilities) ==========
170
181
  const SKILLS = [
171
- {
172
- id: 'web-search',
173
- name: 'Web Search',
174
- icon: '🔍',
175
- descVi: 'Tìm kiếm web, trả về kết quả realtime', descEn: 'Web search, returns realtime results',
176
- slug: 'web-search',
177
- noteVi: 'Cần API key (Tavily/SerpApi) trong .env', noteEn: 'Requires API key (Tavily/SerpApi) in .env',
178
- envVars: ['TAVILY_API_KEY=<your_tavily_key>'],
179
- },
182
+ // Web Search removed — OpenClaw has native search built-in (no Tavily key needed)
180
183
  {
181
184
  id: 'browser',
182
- name: 'Browser Automation',
185
+ name: 'Browser Automation ⭐(Khuyên dùng)',
183
186
  icon: '🌐',
184
187
  descVi: 'Tự động thao tác trình duyệt (Playwright)', descEn: 'Automated browser control (Playwright)',
185
188
  slug: 'browser-automation',
@@ -187,11 +190,17 @@
187
190
  },
188
191
  {
189
192
  id: 'memory',
190
- name: 'Long-term Memory',
193
+ name: 'Long-term Memory ⭐(Khuyên dùng)',
191
194
  icon: '🧠',
192
195
  descVi: 'Nhớ hội thoại xuyên phiên, context dài hạn', descEn: 'Cross-session memory, long-term context',
193
196
  slug: 'memory',
194
197
  },
198
+ {
199
+ id: 'scheduler',
200
+ name: 'Native Cron Scheduler ⭐(Khuyên dùng)',
201
+ icon: '⏰',
202
+ descVi: 'Gọi Cron gốc trên nền tảng (không tải qua HUB)', descEn: 'Native Cron background jobs (No skill download)',
203
+ },
195
204
  {
196
205
  id: 'rag',
197
206
  name: 'RAG / Knowledge Base',
@@ -209,12 +218,6 @@
209
218
  noteVi: 'Dùng chung OPENAI_API_KEY (DALL-E) hoặc thêm FLUX_API_KEY', noteEn: 'Uses OPENAI_API_KEY (DALL-E) or FLUX_API_KEY',
210
219
  envVars: ['# FLUX_API_KEY=<your_flux_key> # chỉ cần nếu dùng Flux'],
211
220
  },
212
- {
213
- id: 'scheduler',
214
- name: 'Native Cron Scheduler',
215
- icon: '⏰',
216
- descVi: 'Gọi Cron gốc trên nền tảng (không tải qua HUB)', descEn: 'Native Cron background jobs (No skill download)',
217
- },
218
221
  {
219
222
  id: 'code-interpreter',
220
223
  name: 'Code Interpreter',
@@ -381,6 +384,7 @@
381
384
 
382
385
  function init() {
383
386
  bindChannelCards();
387
+ bindDeployModeCards();
384
388
  bindNavButtons();
385
389
  bindFormEvents();
386
390
  renderProviderCards();
@@ -390,6 +394,7 @@
390
394
  updateUI();
391
395
  }
392
396
 
397
+
393
398
  // ========== Security Rules Toggle ==========
394
399
  function initSecurityRules() {
395
400
  const textarea = document.getElementById('cfg-security');
@@ -466,28 +471,383 @@
466
471
  }
467
472
  };
468
473
 
469
- // ========== Step 1: Channel Selection ==========
474
+ // ========== Step 2: Channel Selection ==========
470
475
  function bindChannelCards() {
471
- document.querySelectorAll('.channel-card').forEach((card) => {
476
+ const step2 = document.querySelector('.step[data-step="2"]');
477
+ if (!step2) return;
478
+ step2.querySelectorAll('.channel-card[data-channel]').forEach((card) => {
472
479
  card.addEventListener('click', () => {
473
480
  state.channel = card.dataset.channel;
474
- document.querySelectorAll('.channel-card').forEach((c) => c.classList.remove('channel-card--selected'));
481
+ step2.querySelectorAll('.channel-card[data-channel]').forEach((c) => c.classList.remove('channel-card--selected'));
475
482
  card.classList.add('channel-card--selected');
483
+
484
+ // Show multi-bot panel only for Telegram
485
+ const multibotPanel = document.getElementById('multibot-panel');
486
+ if (multibotPanel) {
487
+ multibotPanel.style.display = state.channel === 'telegram' ? '' : 'none';
488
+ }
489
+
476
490
  updateNavButtons();
477
491
  });
478
492
  });
479
493
  }
480
494
 
495
+ // ========== Multi-Bot State & Logic ==========
496
+
497
+ // Extend state with multi-bot fields (lazily added to avoid breaking single-bot)
498
+ state.botCount = 1;
499
+ state.activeBotIndex = 0;
500
+ state.bots = [{ name: '', slashCmd: '', desc: '', provider: 'google', model: 'google/gemini-2.5-flash', token: '', apiKey: '' }];
501
+ state.groupId = '';
502
+
503
+ window.__selectBotCount = function(count) {
504
+ state.botCount = count;
505
+
506
+ // Update button styles
507
+ document.querySelectorAll('#botcount-grid .botcount-btn').forEach(btn => {
508
+ const isActive = parseInt(btn.dataset.count) === count;
509
+ btn.style.border = isActive ? '1px solid rgba(99,102,241,0.5)' : '1px solid rgba(255,255,255,0.12)';
510
+ btn.style.background = isActive ? 'rgba(99,102,241,0.15)' : 'transparent';
511
+ btn.style.color = isActive ? 'var(--text-primary)' : 'var(--text-secondary)';
512
+ });
513
+
514
+ // Ensure bots array has enough entries
515
+ while (state.bots.length < count) {
516
+ state.bots.push({ name: '', slashCmd: '', desc: '', provider: 'google', model: 'google/gemini-2.5-flash', token: '', apiKey: '' });
517
+ }
518
+
519
+ // Show/hide group option for 2+ bots
520
+ const groupOpt = document.getElementById('multibot-group-option');
521
+ if (groupOpt) groupOpt.style.display = count > 1 ? '' : 'none';
522
+
523
+ // Hide/show global bot name + desc fields when multi-bot (each bot has its own in tab panel)
524
+ const identityGrid = document.querySelector('.identity-grid');
525
+ if (identityGrid) {
526
+ const nameField = identityGrid.querySelector('.form-group:has(#cfg-name)');
527
+ const descField = identityGrid.querySelector('.form-group:has(#cfg-desc)');
528
+ if (nameField) nameField.style.display = count > 1 ? 'none' : '';
529
+ if (descField) descField.style.display = count > 1 ? 'none' : '';
530
+ }
531
+
532
+ // Refresh tab bar in Step 3 when already there
533
+ renderBotTabBar();
534
+ };
535
+
536
+ // ── Group option card toggle ─────────────────────────────────────────────
537
+ window.__onGroupOptionChange = function() {
538
+ const isExisting = document.getElementById('group-opt-existing')?.checked;
539
+
540
+ // Cards
541
+ const cardCreate = document.getElementById('group-card-create');
542
+ const cardExisting = document.getElementById('group-card-existing');
543
+ const checkCreate = document.getElementById('group-card-create-check');
544
+ const checkExisting= document.getElementById('group-card-existing-check');
545
+
546
+ if (cardCreate) {
547
+ cardCreate.style.background = isExisting ? 'rgba(255,255,255,0.03)' : 'rgba(16,185,129,0.08)';
548
+ cardCreate.style.borderColor = isExisting ? 'rgba(255,255,255,0.1)' : 'rgba(16,185,129,0.5)';
549
+ }
550
+ if (cardExisting) {
551
+ cardExisting.style.background = isExisting ? 'rgba(99,102,241,0.10)' : 'rgba(255,255,255,0.03)';
552
+ cardExisting.style.borderColor = isExisting ? 'rgba(99,102,241,0.55)' : 'rgba(255,255,255,0.1)';
553
+ }
554
+ if (checkCreate) {
555
+ checkCreate.style.background = isExisting ? 'rgba(255,255,255,0.12)' : 'var(--accent)';
556
+ checkCreate.style.border = isExisting ? '1.5px solid rgba(255,255,255,0.2)' : 'none';
557
+ checkCreate.querySelector('path').setAttribute('opacity', isExisting ? '.3' : '1');
558
+ }
559
+ if (checkExisting) {
560
+ checkExisting.style.background = isExisting ? 'rgb(99,102,241)' : 'rgba(255,255,255,0.12)';
561
+ checkExisting.style.border = isExisting ? 'none' : '1.5px solid rgba(255,255,255,0.2)';
562
+ checkExisting.querySelector('path').setAttribute('opacity', isExisting ? '1' : '.3');
563
+ }
564
+
565
+ // Show/hide Group ID input
566
+ const wrap = document.getElementById('group-id-wrap');
567
+ if (wrap) wrap.style.display = isExisting ? '' : 'none';
568
+ };
569
+
570
+ // Keep legacy alias in case old HTML references remain
571
+ window.__toggleGroupIdInput = (show) => {
572
+ const wrap = document.getElementById('group-id-wrap');
573
+ if (wrap) wrap.style.display = show ? '' : 'none';
574
+ };
575
+
576
+ window.__saveGroupId = function(val) {
577
+ state.groupId = val;
578
+ };
579
+
580
+
581
+ function renderBotTabBar() {
582
+ const tabBar = document.getElementById('bot-tab-bar');
583
+ const tabsEl = document.getElementById('bot-tabs');
584
+ const labelEl = document.getElementById('multibot-tab-label');
585
+ const slashGroup = document.getElementById('slash-cmd-group');
586
+ if (!tabBar || !tabsEl) return;
587
+
588
+ tabBar.style.display = 'block';
589
+
590
+ if (state.botCount <= 1) {
591
+ tabsEl.style.display = 'none';
592
+ if (labelEl) labelEl.style.display = 'none';
593
+ if (slashGroup) slashGroup.style.display = 'none';
594
+
595
+ // Update fields
596
+ const bot = state.bots[0] || { name: 'Bot 1', desc: '', persona: '', slashCmd: '' };
597
+ document.getElementById('cfg-bot-tab-name').value = bot.name || '';
598
+ document.getElementById('cfg-bot-tab-desc').value = bot.desc || '';
599
+ document.getElementById('cfg-bot-tab-persona').value = bot.persona || '';
600
+ return;
601
+ }
602
+
603
+ tabsEl.style.display = 'flex';
604
+ if (labelEl) labelEl.style.display = 'block';
605
+ if (slashGroup) slashGroup.style.display = 'block';
606
+ const lang = document.getElementById('cfg-language')?.value || 'vi';
607
+
608
+ tabsEl.innerHTML = Array.from({ length: state.botCount }, (_, i) => {
609
+ const bot = state.bots[i] || {};
610
+ const label = bot.name || (lang === 'vi' ? `Bot ${i + 1}` : `Bot ${i + 1}`);
611
+ const isActive = i === state.activeBotIndex;
612
+ return `<button onclick="window.__switchBotTab(${i})" style="
613
+ padding:7px 18px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;
614
+ border:1px solid ${isActive ? 'rgba(99,102,241,0.6)' : 'rgba(255,255,255,0.12)'};
615
+ background:${isActive ? 'rgba(99,102,241,0.2)' : 'transparent'};
616
+ color:${isActive ? 'var(--text-primary)' : 'var(--text-secondary)'};
617
+ transition:all 0.15s;">${label}</button>`;
618
+ }).join('');
619
+
620
+ // Populate meta fields for active bot
621
+ syncBotTabMeta();
622
+ }
623
+
624
+ function syncBotTabMeta() {
625
+ const bot = state.bots[state.activeBotIndex] || {};
626
+ const nameEl = document.getElementById('cfg-bot-tab-name');
627
+ const slashEl = document.getElementById('cfg-bot-tab-slash');
628
+ const descEl = document.getElementById('cfg-bot-tab-desc');
629
+ if (nameEl) nameEl.value = bot.name || '';
630
+ if (slashEl) slashEl.value = bot.slashCmd || '';
631
+ if (descEl) descEl.value = bot.desc || '';
632
+
633
+ // Also sync global config fields from active bot (provider/model carry over)
634
+ if (bot.provider) {
635
+ state.config.provider = bot.provider;
636
+ state.config.model = bot.model || 'google/gemini-2.5-flash';
637
+ }
638
+ }
639
+
640
+ window.__switchBotTab = function(index) {
641
+ // Save current tab data first
642
+ saveFormData();
643
+ saveBotTabMeta();
644
+
645
+ // Sync provider/model from global config back to the bot being left
646
+ if (state.bots[state.activeBotIndex]) {
647
+ state.bots[state.activeBotIndex].provider = state.config.provider;
648
+ state.bots[state.activeBotIndex].model = state.config.model;
649
+ }
650
+
651
+ state.activeBotIndex = index;
652
+ renderBotTabBar();
653
+
654
+ // Reload provider/model for newly selected bot
655
+ const bot = state.bots[index] || {};
656
+ state.config.provider = bot.provider || 'google';
657
+ state.config.model = bot.model || 'google/gemini-2.5-flash';
658
+ window.__selectProvider(state.config.provider);
659
+ const mdSel = document.getElementById('cfg-model');
660
+ if (mdSel && state.config.model) {
661
+ const opt = mdSel.querySelector(`option[value="${state.config.model}"]`);
662
+ if (opt) mdSel.value = state.config.model;
663
+ }
664
+ };
665
+
666
+ function saveBotTabMeta() {
667
+ const bot = state.bots[state.activeBotIndex];
668
+ if (!bot) return;
669
+ const nameEl = document.getElementById('cfg-bot-tab-name');
670
+ const slashEl = document.getElementById('cfg-bot-tab-slash');
671
+ const descEl = document.getElementById('cfg-bot-tab-desc');
672
+ if (nameEl) bot.name = nameEl.value;
673
+ if (slashEl) bot.slashCmd = slashEl.value;
674
+ if (descEl) bot.desc = descEl.value;
675
+ }
676
+
677
+ window.__saveBotTabName = function(val) {
678
+ if (state.bots[state.activeBotIndex]) {
679
+ state.bots[state.activeBotIndex].name = val;
680
+ // Update tab label live
681
+ const tabs = document.querySelectorAll('#bot-tabs button');
682
+ if (tabs[state.activeBotIndex]) {
683
+ tabs[state.activeBotIndex].textContent = val || `Bot ${state.activeBotIndex + 1}`;
684
+ }
685
+ }
686
+ };
687
+
688
+ window.__saveBotTabSlash = function(val) {
689
+ if (state.bots[state.activeBotIndex]) {
690
+ state.bots[state.activeBotIndex].slashCmd = val;
691
+ }
692
+ };
693
+
694
+ window.__saveBotTabDesc = function(val) {
695
+ if (state.bots[state.activeBotIndex]) {
696
+ state.bots[state.activeBotIndex].desc = val;
697
+ }
698
+ };
699
+
700
+
701
+
702
+ // ========== Step 1: Deploy Mode + OS ==========
703
+ // ========== OS Advisory Data ==========
704
+ const OS_ADVISORY = {
705
+ win: {
706
+ icon: '🪟',
707
+ titleVi: 'Windows — Khuyên dùng Docker',
708
+ titleEn: 'Windows — Recommended: Docker',
709
+ descVi: 'Bot chạy trong container isolation. Script <code>.bat</code> tự động cài Docker Desktop, pull model, build &amp; start — không cần thao tác thủ công.',
710
+ descEn: 'Bot runs in container isolation. The <code>.bat</code> script auto-installs Docker Desktop, pulls model, builds &amp; starts — fully hands-free.',
711
+ deploy: 'docker',
712
+ badgeVi: '🐳 Docker',
713
+ badgeEn: '🐳 Docker',
714
+ badgeStyle: 'background: rgba(16,185,129,0.15); color: #10b981; border: 1px solid rgba(16,185,129,0.3);',
715
+ },
716
+ linux: {
717
+ icon: '🍎',
718
+ titleVi: 'macOS — Khuyên dùng Docker',
719
+ titleEn: 'macOS — Recommended: Docker',
720
+ descVi: 'Docker Desktop trên macOS chạy ổn định. Script <code>.sh</code> tự cài mọi thứ — Node.js, Docker, model, bot. Chạy một lần, xong!',
721
+ descEn: 'Docker Desktop on macOS is stable. The <code>.sh</code> script auto-installs everything — Node.js, Docker, model, bot. Run once, done!',
722
+ deploy: 'docker',
723
+ badgeVi: '🐳 Docker',
724
+ badgeEn: '🐳 Docker',
725
+ badgeStyle: 'background: rgba(16,185,129,0.15); color: #10b981; border: 1px solid rgba(16,185,129,0.3);',
726
+ },
727
+ vps: {
728
+ icon: '🐧',
729
+ titleVi: 'Ubuntu / VPS — Khuyên dùng Native (Không Docker)',
730
+ titleEn: 'Ubuntu / VPS — Recommended: Native (No Docker)',
731
+ descVi: 'Chạy thẳng trên máy, tiết kiệm RAM, khởi động nhanh. Script tự cài Node.js 20 LTS, PM2, 9Router/Ollama và giữ bot chạy liên tục sau reboot.',
732
+ descEn: 'Run directly on machine — lower RAM, faster startup. Script auto-installs Node.js 20 LTS, PM2, 9Router/Ollama and keeps bot running across reboots.',
733
+ deploy: 'native',
734
+ badgeVi: '💻 Native + PM2',
735
+ badgeEn: '💻 Native + PM2',
736
+ badgeStyle: 'background: rgba(245,158,11,0.15); color: #f59e0b; border: 1px solid rgba(245,158,11,0.3);',
737
+ },
738
+ 'linux-desktop': {
739
+ icon: '🖥️',
740
+ titleVi: 'Linux Desktop — Khuyên dùng Native',
741
+ titleEn: 'Linux Desktop — Recommended: Native',
742
+ descVi: 'Không cần Docker. Script tự cài Node.js 20 LTS nếu chưa có, rồi cài 9Router hoặc Ollama theo lựa chọn provider của bạn.',
743
+ descEn: 'No Docker needed. Script auto-installs Node.js 20 LTS if missing, then installs 9Router or Ollama based on your provider choice.',
744
+ deploy: 'native',
745
+ badgeVi: '💻 Native',
746
+ badgeEn: '💻 Native',
747
+ badgeStyle: 'background: rgba(245,158,11,0.15); color: #f59e0b; border: 1px solid rgba(245,158,11,0.3);',
748
+ },
749
+ };
750
+
751
+ function bindDeployModeCards() {
752
+ // Override deploy mode cards (inside advanced panel)
753
+ document.querySelectorAll('#deploy-mode-grid .channel-card').forEach(function(card) {
754
+ card.addEventListener('click', function() {
755
+ state.deployMode = card.dataset.deploy;
756
+ document.querySelectorAll('#deploy-mode-grid .channel-card').forEach(function(c) { c.classList.remove('channel-card--selected'); });
757
+ card.classList.add('channel-card--selected');
758
+ updateDockerNotice();
759
+ // Update advisory badge to reflect manual override
760
+ var lang = document.getElementById('cfg-language')?.value || 'vi';
761
+ var adv = document.getElementById('env-adv-badge');
762
+ if (adv) {
763
+ if (state.deployMode === 'docker') {
764
+ adv.textContent = '🐳 Docker';
765
+ adv.style.cssText = 'flex-shrink:0; padding:4px 12px; border-radius:20px; font-size:12px; font-weight:600; white-space:nowrap; background:rgba(16,185,129,0.15); color:#10b981; border:1px solid rgba(16,185,129,0.3);';
766
+ } else {
767
+ adv.textContent = '💻 Native';
768
+ adv.style.cssText = 'flex-shrink:0; padding:4px 12px; border-radius:20px; font-size:12px; font-weight:600; white-space:nowrap; background:rgba(245,158,11,0.15); color:#f59e0b; border:1px solid rgba(245,158,11,0.3);';
769
+ }
770
+ }
771
+ });
772
+ });
773
+ // Initial advisory render
774
+ updateAdvisory(state.nativeOs);
775
+ }
776
+
777
+ function updateAdvisory(os) {
778
+ var lang = document.getElementById('cfg-language')?.value || 'vi';
779
+ var data = OS_ADVISORY[os] || OS_ADVISORY['win'];
780
+
781
+ // Set deploy mode automatically from recommendation
782
+ state.deployMode = data.deploy;
783
+
784
+ var icon = document.getElementById('env-adv-icon');
785
+ var title = document.getElementById('env-adv-title');
786
+ var desc = document.getElementById('env-adv-desc');
787
+ var badge = document.getElementById('env-adv-badge');
788
+
789
+ if (icon) icon.textContent = data.icon;
790
+ if (title) title.textContent = lang === 'vi' ? data.titleVi : data.titleEn;
791
+ if (desc) desc.innerHTML = lang === 'vi' ? data.descVi : data.descEn;
792
+ if (badge) {
793
+ badge.textContent = lang === 'vi' ? data.badgeVi : data.badgeEn;
794
+ badge.style.cssText = 'flex-shrink:0; padding:4px 12px; border-radius:20px; font-size:12px; font-weight:600; white-space:nowrap; ' + data.badgeStyle;
795
+ }
796
+
797
+ // Sync override panel selection
798
+ document.querySelectorAll('#deploy-mode-grid .channel-card').forEach(function(c) {
799
+ c.classList.toggle('channel-card--selected', c.dataset.deploy === state.deployMode);
800
+ });
801
+
802
+ updateDockerNotice();
803
+ }
804
+
805
+ window.__selectOs = function(os) {
806
+ state.nativeOs = os;
807
+ // Highlight selected OS card
808
+ document.querySelectorAll('#native-os-grid .channel-card').forEach(function(c) {
809
+ c.classList.remove('channel-card--selected');
810
+ });
811
+ var card = document.querySelector('#native-os-grid .channel-card[data-os="' + os + '"]');
812
+ if (card) card.classList.add('channel-card--selected');
813
+ updateAdvisory(os);
814
+ };
815
+
816
+ window.__toggleDeployOverride = function() {
817
+ var panel = document.getElementById('deploy-override-panel');
818
+ var btn = document.getElementById('btn-deploy-toggle');
819
+ if (!panel) return;
820
+ var lang = document.getElementById('cfg-language')?.value || 'vi';
821
+ var isOpen = panel.style.display !== 'none';
822
+ panel.style.display = isOpen ? 'none' : '';
823
+ if (btn) {
824
+ btn.querySelector('span').textContent = isOpen
825
+ ? (lang === 'vi' ? 'Tuỳ chỉnh ▾' : 'Customize ▾')
826
+ : (lang === 'vi' ? 'Thu gọn ▴' : 'Collapse ▴');
827
+ }
828
+ };
829
+
830
+ function updateDockerNotice() {
831
+ var notice = document.getElementById('docker-install-notice');
832
+ var winNotice = document.getElementById('docker-win-notice');
833
+ if (!notice) return;
834
+ notice.style.display = state.deployMode === 'docker' ? '' : 'none';
835
+ if (winNotice) winNotice.style.display = (state.deployMode === 'docker' && state.nativeOs === 'win') ? '' : 'none';
836
+ }
837
+
838
+
481
839
  // ========== Navigation ==========
482
840
  function bindNavButtons() {
483
841
  const btnNext = document.getElementById('btn-next');
484
842
  const btnPrev = document.getElementById('btn-prev');
485
843
 
486
844
  btnNext.addEventListener('click', () => {
487
- if (state.currentStep === 1 && !state.channel) return;
488
- if (state.currentStep === 2) saveFormData();
489
-
490
-
845
+ // Step 2 requires channel selection
846
+ if (state.currentStep === 2 && !state.channel) return;
847
+ // Step 3 (bot config) — save form
848
+ if (state.currentStep === 3) saveFormData();
849
+ // Step 4 (credentials) — save before moving to step 5
850
+ if (state.currentStep === 4) saveCredentials();
491
851
  if (state.currentStep < state.totalSteps) {
492
852
  goToStep(state.currentStep + 1);
493
853
  }
@@ -495,6 +855,9 @@
495
855
 
496
856
  btnPrev.addEventListener('click', () => {
497
857
  if (state.currentStep > 1) {
858
+ // Save current step data before going back
859
+ if (state.currentStep === 3) saveFormData();
860
+ if (state.currentStep === 4) saveCredentials();
498
861
  goToStep(state.currentStep - 1);
499
862
  }
500
863
  });
@@ -502,9 +865,10 @@
502
865
 
503
866
  function goToStep(step) {
504
867
  state.currentStep = step;
505
- if (step === 2) populateStep2();
506
- if (step === 3) populateStep3();
507
- if (step === 4) generateOutput();
868
+ // 1=env/deploy, 2=channel, 3=bot config, 4=api keys, 5=output
869
+ if (step === 3) populateStep2(); // bot config
870
+ if (step === 4) populateStep3(); // api keys
871
+ if (step === 5) generateOutput(); // output
508
872
  updateUI();
509
873
  }
510
874
 
@@ -539,35 +903,38 @@
539
903
  } else {
540
904
  const lang = document.getElementById('cfg-language')?.value || 'vi';
541
905
  btnNext.style.display = '';
542
-
906
+
543
907
  let isDisabled = false;
544
- if (state.currentStep === 1 && !state.channel) isDisabled = true;
545
- if (state.currentStep === 2) {
908
+ // Step 1 (env): always valid
909
+ // Step 2 (channel): require selection
910
+ if (state.currentStep === 2 && !state.channel) isDisabled = true;
911
+ // Step 3 (bot config): require bot name
912
+ if (state.currentStep === 3) {
546
913
  const nameVal = document.getElementById('cfg-name')?.value?.trim();
547
- if (!nameVal) isDisabled = true;
914
+ const userInfoVal = document.getElementById('cfg-user-info')?.value?.trim();
915
+ if (!nameVal || !userInfoVal) isDisabled = true;
548
916
  }
549
- if (state.currentStep === 3) {
917
+ // Step 4 (api keys): require token/key
918
+ if (state.currentStep === 4) {
550
919
  const botTokenEl = document.getElementById('key-bot-token');
551
920
  const apiKeyEl = document.getElementById('key-api-key');
552
-
553
921
  const provider = PROVIDERS[state.config.provider];
554
-
555
922
  if ((state.channel === 'telegram' || state.channel === 'zalo-bot') && botTokenEl) {
556
923
  if (!botTokenEl.value.trim()) isDisabled = true;
557
924
  }
558
-
559
925
  if (provider && !provider.isProxy && !provider.isLocal && provider.envKey && apiKeyEl) {
560
926
  if (!apiKeyEl.value.trim()) isDisabled = true;
561
927
  }
562
928
  }
563
929
 
564
930
  btnNext.disabled = isDisabled;
565
- btnNextLabel.textContent = state.currentStep === 3
566
- ? (lang === 'vi' ? 'Generate Configs' : 'Generate Configs')
931
+ btnNextLabel.textContent = state.currentStep === 4
932
+ ? (lang === 'vi' ? 'Generate Configs' : 'Generate Configs')
567
933
  : (lang === 'vi' ? 'Tiếp theo' : 'Next');
568
934
  }
569
935
  }
570
936
 
937
+
571
938
  // ========== Step 2: Bot Config ==========
572
939
  function renderProviderCards() {
573
940
  const grid = document.getElementById('provider-grid');
@@ -696,39 +1063,119 @@
696
1063
  }
697
1064
 
698
1065
  function populateStep2() {
699
- const prompt = document.getElementById('cfg-prompt');
700
1066
  const lang = document.getElementById('cfg-language')?.value || 'vi';
701
- const name = document.getElementById('cfg-name')?.value || 'Bot';
702
- const desc = document.getElementById('cfg-desc')?.value || (lang === 'vi' ? 'trợ lý AI cá nhân' : 'a personal AI assistant');
703
- if (prompt && !prompt.dataset.userEdited) {
704
- prompt.value = DEFAULT_PROMPTS[lang].replace('{BOT_NAME}', name).replace('{BOT_DESC}', desc);
1067
+
1068
+ // Restore saved text fields
1069
+ const nameEl = document.getElementById('cfg-name');
1070
+ const descEl = document.getElementById('cfg-desc');
1071
+ const emojiEl = document.getElementById('cfg-emoji');
1072
+ const userInfoEl = document.getElementById('cfg-user-info');
1073
+ if (nameEl && state.config.botName) nameEl.value = state.config.botName;
1074
+ if (descEl && state.config.description) descEl.value = state.config.description;
1075
+ if (emojiEl && state.config.emoji) emojiEl.value = state.config.emoji;
1076
+ if (userInfoEl && state.config.userInfo) userInfoEl.value = state.config.userInfo;
1077
+
1078
+ // Prompt: restore user-edited, or auto-generate from name+desc
1079
+ const prompt = document.getElementById('cfg-prompt');
1080
+ if (prompt) {
1081
+ if (state.config.systemPrompt) {
1082
+ prompt.value = state.config.systemPrompt;
1083
+ prompt.dataset.userEdited = 'true';
1084
+ } else if (!prompt.dataset.userEdited) {
1085
+ const name = nameEl?.value || 'Bot';
1086
+ const desc = descEl?.value || (lang === 'vi' ? 'trợ lý AI cá nhân' : 'a personal AI assistant');
1087
+ prompt.value = DEFAULT_PROMPTS[lang].replace('{BOT_NAME}', name).replace('{BOT_DESC}', desc);
1088
+ }
705
1089
  setTimeout(() => { prompt.style.height = 'auto'; prompt.style.height = prompt.scrollHeight + 'px'; }, 50);
706
1090
  }
707
- // Update security rules language
708
- renderPluginGrid(); renderProviderCards();
1091
+
1092
+ // Security rules
709
1093
  const securityEl = document.getElementById('cfg-security');
710
- if (securityEl && !securityEl.dataset.userEdited) {
711
- securityEl.value = DEFAULT_SECURITY_RULES[lang];
1094
+ if (securityEl) {
1095
+ if (state.config.securityRules) {
1096
+ securityEl.value = state.config.securityRules;
1097
+ securityEl.dataset.userEdited = 'true';
1098
+ } else if (!securityEl.dataset.userEdited) {
1099
+ securityEl.value = DEFAULT_SECURITY_RULES[lang];
1100
+ }
712
1101
  }
1102
+
1103
+ // Render cards, then restore selections
1104
+ renderProviderCards();
1105
+ // Restore provider selection (highlight card + populate model dropdown)
1106
+ window.__selectProvider(state.config.provider || 'google');
1107
+ // Restore exact model that was selected
1108
+ const modelSelect = document.getElementById('cfg-model');
1109
+ if (modelSelect && state.config.model) {
1110
+ const opt = modelSelect.querySelector(`option[value="${state.config.model}"]`);
1111
+ if (opt) modelSelect.value = state.config.model;
1112
+ }
1113
+
1114
+ // Render plugin/skill grids and restore checked state
1115
+ renderPluginGrid();
1116
+ // Restore skill selections
1117
+ state.config.skills.forEach(sid => {
1118
+ const card = document.querySelector(`.plugin-card[data-skill="${sid}"]`);
1119
+ if (card) {
1120
+ card.classList.add('plugin-card--selected');
1121
+ const cb = card.querySelector('input[type="checkbox"]');
1122
+ if (cb) cb.checked = true;
1123
+ }
1124
+ });
1125
+ // Restore plugin selections
1126
+ state.config.plugins.forEach(pid => {
1127
+ const card = document.querySelector(`.plugin-card[data-plugin="${pid}"]`);
1128
+ if (card) {
1129
+ card.classList.add('plugin-card--selected');
1130
+ const cb = card.querySelector('input[type="checkbox"]');
1131
+ if (cb) cb.checked = true;
1132
+ }
1133
+ });
1134
+
713
1135
  const channelLabel = document.getElementById('selected-channel-label');
714
1136
  if (channelLabel && state.channel) {
715
1137
  channelLabel.textContent = CHANNELS[state.channel].name;
716
1138
  }
717
- // Select Google by default
718
- window.__selectProvider(state.config.provider || 'google');
1139
+
1140
+ // Render bot tab bar (visible only in multi-bot mode)
1141
+ renderBotTabBar();
719
1142
  }
720
1143
 
721
1144
  function saveFormData() {
722
- state.config.botName = document.getElementById('cfg-name')?.value || 'Chat Bot';
723
- state.config.description = document.getElementById('cfg-desc')?.value || 'Personal AI assistant';
724
- state.config.emoji = document.getElementById('cfg-emoji')?.value || '🤖';
725
- state.config.model = document.getElementById('cfg-model')?.value || 'google/gemini-2.5-flash';
726
- state.config.language = document.getElementById('cfg-language')?.value || 'vi';
727
- state.config.systemPrompt = document.getElementById('cfg-prompt')?.value || DEFAULT_PROMPTS['vi'];
728
- state.config.userInfo = document.getElementById('cfg-user-info')?.value || '';
729
- state.config.securityRules = document.getElementById('cfg-security')?.value || DEFAULT_SECURITY_RULES['vi'];
1145
+ state.config.botName = document.getElementById('cfg-name')?.value || state.config.botName || 'Chat Bot';
1146
+ state.config.description = document.getElementById('cfg-desc')?.value || state.config.description || 'Personal AI assistant';
1147
+ state.config.emoji = document.getElementById('cfg-emoji')?.value || state.config.emoji || '🤖';
1148
+ state.config.model = document.getElementById('cfg-model')?.value || state.config.model || 'google/gemini-2.5-flash';
1149
+ state.config.language = document.getElementById('cfg-language')?.value || state.config.language || 'vi';
1150
+ state.config.systemPrompt = document.getElementById('cfg-prompt')?.value || state.config.systemPrompt || DEFAULT_PROMPTS['vi'];
1151
+ state.config.userInfo = document.getElementById('cfg-user-info')?.value?.trim() || state.config.userInfo || '';
1152
+ state.config.securityRules = document.getElementById('cfg-security')?.value || state.config.securityRules || DEFAULT_SECURITY_RULES['vi'];
1153
+ }
1154
+
1155
+ // Save Step 4 credential inputs to state (persists across Back navigation)
1156
+ function saveCredentials() {
1157
+ const botTokenEl = document.getElementById('key-bot-token');
1158
+ const apiKeyEl = document.getElementById('key-api-key');
1159
+ const pathEl = document.getElementById('cfg-project-path');
1160
+ if (botTokenEl) state.config.botToken = botTokenEl.value;
1161
+ if (apiKeyEl) state.config.apiKey = apiKeyEl.value;
1162
+ if (pathEl) state.config.projectPath = pathEl.value;
1163
+
1164
+ // Also save multi-bot tokens individually
1165
+ if (state.botCount > 1) {
1166
+ for (let i = 0; i < state.botCount; i++) {
1167
+ const el = document.getElementById(`key-bot-token-${i}`);
1168
+ if (el && state.bots[i]) state.bots[i].token = el.value;
1169
+ }
1170
+ }
730
1171
  }
731
1172
 
1173
+ // Save a specific bot's token directly (called from oninput)
1174
+ window.__saveBotToken = function(index, val) {
1175
+ if (state.bots[index]) state.bots[index].token = val;
1176
+ };
1177
+
1178
+
732
1179
  // ========== Step 3: Credentials ==========
733
1180
  function populateStep3() {
734
1181
  const ch = CHANNELS[state.channel];
@@ -773,8 +1220,15 @@
773
1220
  </p>`;
774
1221
  } else if (provider.isLocal) {
775
1222
  // Ollama
776
- pHtml += `<p style="font-size: 13px; color: var(--text-secondary); margin: 0;">
777
- ${isVi ? 'Đảm bảo <code>ollama serve</code> đang chạy trên máy trước khi start Docker.' : 'Make sure <code>ollama serve</code> is running before starting Docker.'}
1223
+ pHtml += `<p style="font-size: 13px; color: var(--text-secondary); margin: 0 0 8px;">
1224
+ ${isVi
1225
+ ? '🐳 Ollama sẽ tự chạy trong Docker cùng bot. Model được tải tự động khi <code>docker compose up</code>.'
1226
+ : '🐳 Ollama runs automatically as a Docker sidecar. Model is pulled automatically on first <code>docker compose up</code>.'}
1227
+ </p>`;
1228
+ pHtml += `<p style="font-size: 12px; color: var(--text-muted); margin: 4px 0 0;">
1229
+ ${isVi
1230
+ ? '💡 <b>Chọn model phù hợp với RAM:</b> gemma4:e2b (~4-6 GB), gemma4:e4b (~8-10 GB), gemma4:26b (~18-24 GB), gemma4:31b (~24+ GB). macOS M-chip: GPU không dùng được trong Docker, chạy CPU-only.'
1231
+ : '💡 <b>Pick model by RAM:</b> gemma4:e2b (~4-6 GB), gemma4:e4b (~8-10 GB), gemma4:26b (~18-24 GB), gemma4:31b (~24+ GB). macOS Apple Silicon: GPU unavailable in Docker, CPU-only mode.'}
778
1232
  </p>`;
779
1233
  } else {
780
1234
  // Direct API provider: show key input
@@ -800,11 +1254,28 @@
800
1254
  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>`;
801
1255
 
802
1256
  if (state.channel === 'telegram') {
803
- cHtml += `<div class="form-group" style="margin: 0;">
804
- <label class="form-group__label" for="key-bot-token">🤖 Telegram Bot Token <span style="color: var(--danger, #ef4444);">*</span></label>
805
- <input type="text" class="form-input" id="key-bot-token" placeholder="VD: 1234567890:ABCdefGHIjklMNOpqrsTUVwxyz" style="font-family: monospace; font-size: 13px;" oninput="window.__validateKeys()">
806
- <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>
807
- </div>`;
1257
+ if (state.botCount > 1) {
1258
+ // Multi-bot: one token per bot
1259
+ cHtml += `<div style="display:flex;flex-direction:column;gap:12px;">`;
1260
+ for (let i = 0; i < state.botCount; i++) {
1261
+ const botLabel = state.bots[i]?.name || `Bot ${i + 1}`;
1262
+ const slashTag = state.bots[i]?.slashCmd ? ` <code style="font-size:11px;color:var(--text-muted)">${state.bots[i].slashCmd}</code>` : '';
1263
+ const savedToken = state.bots[i]?.token || '';
1264
+ cHtml += `<div class="form-group" style="margin:0;">
1265
+ <label class="form-group__label" for="key-bot-token-${i}">🤖 ${botLabel}${slashTag} — Bot Token <span style="color:var(--danger,#ef4444)">*</span></label>
1266
+ <input type="text" class="form-input" id="key-bot-token-${i}" value="${savedToken}" placeholder="VD: 1234567890:ABCdefGHI..." style="font-family:monospace;font-size:13px;" oninput="window.__saveBotToken(${i},this.value);window.__validateKeys()">
1267
+ </div>`;
1268
+ }
1269
+ cHtml += `</div>`;
1270
+ cHtml += `<p class="form-group__hint" style="margin-top:8px;">${isVi ? 'Lấy token cho từng bot từ <a href="https://t.me/BotFather" target="_blank">@BotFather</a> — mỗi bot cần 1 token riêng.' : 'Get each token from <a href="https://t.me/BotFather" target="_blank">@BotFather</a> — each bot needs its own token.'}</p>`;
1271
+ } else {
1272
+ // Single bot
1273
+ cHtml += `<div class="form-group" style="margin: 0;">
1274
+ <label class="form-group__label" for="key-bot-token">🤖 Telegram Bot Token <span style="color: var(--danger, #ef4444);">*</span></label>
1275
+ <input type="text" class="form-input" id="key-bot-token" placeholder="VD: 1234567890:ABCdefGHIjklMNOpqrsTUVwxyz" style="font-family: monospace; font-size: 13px;" oninput="window.__validateKeys()">
1276
+ <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>
1277
+ </div>`;
1278
+ }
808
1279
  } else if (state.channel === 'zalo-bot') {
809
1280
  cHtml += `<div class="form-group" style="margin: 0;">
810
1281
  <label class="form-group__label" for="key-bot-token">🔑 Zalo Bot Token <span style="color: var(--danger, #ef4444);">*</span></label>
@@ -824,6 +1295,7 @@
824
1295
  channelEl.innerHTML = cHtml;
825
1296
  }
826
1297
 
1298
+
827
1299
  // ─── Section 3: Skill env vars ───
828
1300
  const skillsEl = document.getElementById('key-section-skills');
829
1301
  if (skillsEl) {
@@ -846,6 +1318,22 @@
846
1318
  });
847
1319
  skillsEl.innerHTML = sHtml;
848
1320
  }
1321
+
1322
+ // ─── Restore persisted credential values (Bug 1 fix) ───
1323
+ // Must run AFTER all innerHTML assignments above so elements exist
1324
+ if (state.config.botToken) {
1325
+ const btEl = document.getElementById('key-bot-token');
1326
+ if (btEl) btEl.value = state.config.botToken;
1327
+ }
1328
+ if (state.config.apiKey) {
1329
+ const akEl = document.getElementById('key-api-key');
1330
+ if (akEl) akEl.value = state.config.apiKey;
1331
+ }
1332
+ // Restore project path
1333
+ if (state.config.projectPath) {
1334
+ const ppEl = document.getElementById('cfg-project-path');
1335
+ if (ppEl) ppEl.value = state.config.projectPath;
1336
+ }
849
1337
  }
850
1338
  window.__validateKeys = function() { updateNavButtons(); };
851
1339
  // 9Router API keys are managed via its dashboard — no client-side generation needed
@@ -861,16 +1349,32 @@
861
1349
 
862
1350
  const lines = [];
863
1351
  const apiKeyVal = document.getElementById('key-api-key')?.value?.trim() || '';
864
- const botTokenVal = document.getElementById('key-bot-token')?.value?.trim() || '';
1352
+ const botTokenVal = document.getElementById('key-bot-token')?.value?.trim()
1353
+ || state.config.botToken || '';
865
1354
 
866
1355
  if (provider.isProxy) {
867
1356
  lines.push('# Không cần AI API key — 9Router xử lý qua dashboard');
868
1357
  } else if (provider.isLocal) {
869
- lines.push('OLLAMA_HOST=http://host.docker.internal:11434');
1358
+ lines.push('OLLAMA_HOST=http://ollama:11434');
1359
+ lines.push('OLLAMA_API_KEY=ollama-local');
870
1360
  } else {
871
1361
  lines.push(`${provider.envKey}=${apiKeyVal || '<your_' + provider.envKey.toLowerCase() + '>'}`);
872
1362
  }
873
- if (ch.envExtra) {
1363
+
1364
+ // Bot tokens
1365
+ if (state.channel === 'telegram' && state.botCount > 1) {
1366
+ // Multi-bot: one env var per bot
1367
+ lines.push('');
1368
+ lines.push('# Multi-bot Telegram tokens');
1369
+ for (let i = 0; i < state.botCount; i++) {
1370
+ const t = state.bots[i]?.token || '';
1371
+ const label = state.bots[i]?.name || `Bot ${i + 1}`;
1372
+ lines.push(`TELEGRAM_BOT_TOKEN_${i + 1}=${t || `<token_for_${label.toLowerCase().replace(/\s+/g, '_')}>`}`);
1373
+ }
1374
+ if (state.groupId) {
1375
+ lines.push(`TELEGRAM_GROUP_ID=${state.groupId}`);
1376
+ }
1377
+ } else if (ch.envExtra) {
874
1378
  if (botTokenVal) {
875
1379
  lines.push(ch.envExtra.replace(/=<[^>]+>$/, '=' + botTokenVal));
876
1380
  } else {
@@ -902,6 +1406,7 @@
902
1406
  envContent.textContent = lines.join('\n');
903
1407
  }
904
1408
 
1409
+
905
1410
  // ========== Step 4: Generate Output ==========
906
1411
  function generateOutput() {
907
1412
  const ch = CHANNELS[state.channel];
@@ -916,6 +1421,28 @@
916
1421
  if (!provider) return;
917
1422
 
918
1423
  const is9Router = provider.isProxy;
1424
+ const isLocal = provider.isLocal;
1425
+ const isTelegramMultiBot = state.botCount > 1 && state.channel === 'telegram';
1426
+ const relayPluginSpec = 'clawhub:openclaw-telegram-multibot-relay';
1427
+
1428
+ function buildRelayPluginInstallCommand(prefix) {
1429
+ return `${prefix} plugins install ${relayPluginSpec} 2>/dev/null || true`;
1430
+ }
1431
+
1432
+ function buildRelayPluginInstallCommandWin(prefix) {
1433
+ return `${prefix} plugins install ${relayPluginSpec} || exit /b 0`;
1434
+ }
1435
+
1436
+ function buildTelegramPostInstallChecklist() {
1437
+ const groupId = state.groupId || '';
1438
+ const botList = state.bots.slice(0, state.botCount).map((bot, idx) => `- **${bot?.name || `Bot ${idx + 1}`}**`).join('\n');
1439
+ const isVi = lang === 'vi';
1440
+ return isVi
1441
+ ? `# Telegram Post-Install Checklist\n\nBot da duoc cai dat. Thuc hien cac buoc sau de hoat dong trong group.\n\n## Group ID\n- ${groupId ? `Group ID: ${groupId}` : 'Chua nhap Group ID - bot se hoat dong o moi group.'}\n\n## Danh sach bot\n${botList}\n\n---\n\n## Buoc 1 -- Tat Privacy Mode tren BotFather (bat buoc, lam truoc)\n\nMac dinh bot chi doc tin nhan bat dau bang /. Phai tat Privacy Mode thi bot moi doc duoc tat ca tin nhan trong group.\n\nLam lan luot cho TUNG BOT:\n1. Mo Telegram, tim @BotFather\n2. Gui: /mybots\n3. Chon bot can sua\n4. Chon: Bot Settings\n5. Chon: Group Privacy\n6. Chon: Turn off\n7. BotFather se bao: "Privacy mode is disabled for ..."\n\n!!! QUAN TRONG: Phai lam buoc nay TRUOC khi add bot vao group. Neu bot da o trong group roi thi phai Remove bot ra, sau do Add lai.\n\n## Buoc 2 -- Add bot vao group\n\nSau khi tat Privacy Mode cho ALL bot:\n1. Mo group Telegram cua ban\n2. Vao Settings -> Members -> Add Members\n3. Tim ten tung bot theo username (VD: @TenCuaBot) va add vao\n4. Sau khi add, vao Settings -> Administrators\n5. Promote tung bot len Admin (can quyen phan hoi, co the de mac dinh)\n\nLay username that cua bot: vao @BotFather -> /mybots -> chon bot -> username la chu sau @.\n\n## Buoc 3 -- Lay Group ID (neu chua co)\n\n1. Them @userinfobot vao group nhu admin\n2. Go /start hoac forward bat ky tin nhan trong group cho @userinfobot\n3. Bot tra ve Chat ID bat dau bang -100...\n4. Dat gia tri do vao TELEGRAM_GROUP_ID trong file .env\n\n## Buoc 4 -- Cai plugin (neu chua cai duoc tu dong)\n\nNeu trong qua trinh setup bao loi cai plugin, sau khi bot dang chay hay chay:\n\n openclaw plugins install ${relayPluginSpec}\n\n## Buoc 5 -- Test\n\n1. Gui tin nhan trong group, mention bot: @TenCuaBot xin chao\n2. Bot se phan hoi\n3. Neu khong phan hoi: kiem tra lai Buoc 1 (Privacy Mode) va Buoc 2 (add lai sau khi tat privacy)\n\n---\n*Generated by OpenClaw Setup*\n`
1442
+ : `# Telegram Post-Install Checklist\n\nBots are installed. Complete the steps below to activate them in a group.\n\n## Group ID\n- ${groupId ? `Group ID: ${groupId}` : 'No Group ID - bots will respond in any group.'}\n\n## Bot list\n${botList}\n\n---\n\n## Step 1 -- Disable Privacy Mode on BotFather (required, do this first)\n\nBy default bots only read messages starting with /. You must disable Privacy Mode so bots can read all group messages.\n\nDo this for EACH BOT:\n1. Open Telegram, find @BotFather\n2. Send: /mybots\n3. Select the bot\n4. Choose: Bot Settings\n5. Choose: Group Privacy\n6. Choose: Turn off\n7. BotFather confirms: "Privacy mode is disabled for ..."\n\n!!! IMPORTANT: Do this BEFORE adding the bot to the group. If the bot is already in the group, remove it first then re-add it.\n\n## Step 2 -- Add bots to the group\n\nAfter disabling Privacy Mode for ALL bots:\n1. Open your Telegram group\n2. Go to Settings -> Members -> Add Members\n3. Search each bot by username (e.g. @YourBotUsername) and add it\n4. Go to Settings -> Administrators\n5. Promote each bot to Admin\n\nTo get each bot's real username: open @BotFather -> /mybots -> select bot -> username after @.\n\n## Step 3 -- Get Group ID (if not already set)\n\n1. Add @userinfobot to the group as admin\n2. Send /start or forward any group message to @userinfobot\n3. It returns a Chat ID starting with -100...\n4. Set that value as TELEGRAM_GROUP_ID in your .env file\n\n## Step 4 -- Install plugin (if auto-install failed)\n\nIf setup reported a plugin install error, run this after the bot is running:\n\n openclaw plugins install ${relayPluginSpec}\n\n## Step 5 -- Test\n\n1. Send a message in the group mentioning the bot: @YourBotUsername hello\n2. The bot should respond\n3. No response? Re-check Step 1 (Privacy Mode) and Step 2 (re-add bot after disabling privacy)\n\n---\n*Generated by OpenClaw Setup*\n`;
1443
+ }
1444
+
1445
+
919
1446
 
920
1447
  // Show/hide 9Router post-setup notice
921
1448
  const routerNotice = document.getElementById('9router-notice');
@@ -983,9 +1510,15 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
983
1510
  setOutput('out-task-ps1', taskPs1);
984
1511
  }
985
1512
 
986
- // Show Docker output
1513
+ // Show/hide docker vs native output based on deployMode
987
1514
  const dockerOut = document.getElementById('docker-output');
988
- if (dockerOut) dockerOut.style.display = '';
1515
+ const nativeOut = document.getElementById('native-output');
1516
+ const isNativeMode = state.deployMode === 'native';
1517
+ if (dockerOut) dockerOut.style.display = isNativeMode ? 'none' : '';
1518
+ if (nativeOut) nativeOut.style.display = isNativeMode ? '' : 'none';
1519
+
1520
+ // Generate native script if native mode
1521
+ if (isNativeMode) generateNativeScript();
989
1522
 
990
1523
  // Show/hide Zalo Personal onboard notice
991
1524
  const zaloNotice = document.getElementById('zalo-onboard-notice');
@@ -995,15 +1528,36 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
995
1528
  if (isZaloPersonal) generateZaloOnboardGuide();
996
1529
  }
997
1530
 
998
- // Reset step 4 heading
1531
+ // Update step 5 heading
1532
+ const lang5 = document.getElementById('cfg-language')?.value || 'vi';
999
1533
  const title = document.getElementById('step4-title');
1000
1534
  const desc = document.getElementById('step4-desc');
1001
- if (title) title.textContent = (document.getElementById('cfg-language')?.value || 'vi') === 'vi' ? '🎉 Config đã sẵn sàng!' : '🎉 Config is Ready!';
1002
- if (desc) desc.textContent = (document.getElementById('cfg-language')?.value || 'vi') === 'vi' ? 'Copy script bên dưới → paste vào terminal trong thư mục project → config được tạo tự động.' : 'Copy the script below → paste into terminal in your project folder → configs created automatically.';
1535
+ if (title) title.textContent = lang5 === 'vi' ? '🎉 Sẵn sàng! Tải script cài đặt' : '🎉 Ready! Download setup script';
1536
+ if (desc) desc.textContent = lang5 === 'vi'
1537
+ ? 'Script đã được tạo theo cấu hình bạn chọn. Tải về và chạy — mọi thứ còn lại được xử lý tự động.'
1538
+ : 'Script is generated from your choices. Download and run — everything else is handled automatically.';
1003
1539
 
1004
1540
  const agentId = state.config.botName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, '') || 'chat';
1005
1541
 
1006
1542
  const hasBrowser = state.config.skills.includes('browser');
1543
+ const isSharedMultiBot = state.botCount > 1 && state.channel === 'telegram';
1544
+ const multiBotAgentMetas = isSharedMultiBot
1545
+ ? state.bots.slice(0, state.botCount).map((bot, idx) => {
1546
+ const name = bot?.name || `Bot ${idx + 1}`;
1547
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `bot-${idx + 1}`;
1548
+ return {
1549
+ idx,
1550
+ name,
1551
+ desc: bot?.desc || state.config.description || (lang5 === 'vi' ? 'Tro ly AI ca nhan' : 'Personal AI assistant'),
1552
+ persona: bot?.persona || '',
1553
+ slashCmd: bot?.slashCmd || '',
1554
+ token: (bot?.token || '').trim(),
1555
+ agentId: slug,
1556
+ accountId: idx === 0 ? 'default' : slug,
1557
+ workspaceDir: `workspace-${slug}`,
1558
+ };
1559
+ })
1560
+ : [];
1007
1561
 
1008
1562
  // 1. openclaw.json
1009
1563
  const clawConfig = {
@@ -1012,6 +1566,8 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1012
1566
  defaults: {
1013
1567
  model: { primary: state.config.model, fallbacks: [] },
1014
1568
  compaction: { mode: 'safeguard' },
1569
+ timeoutSeconds: isLocal ? 900 : 120,
1570
+ ...(isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
1015
1571
  },
1016
1572
  list: [{
1017
1573
  id: agentId,
@@ -1030,13 +1586,16 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1030
1586
  };
1031
1587
 
1032
1588
  // 9Router: add proxy endpoint config under models.providers
1033
- // Bot and 9Router share Docker network no API key needed
1589
+ // Native mode: 9router runs on localhost; Docker mode: uses docker service hostname
1034
1590
  if (is9Router) {
1591
+ const nineRouterBase = state.deployMode === 'native'
1592
+ ? 'http://localhost:20128/v1'
1593
+ : 'http://9router:20128/v1';
1035
1594
  clawConfig.models = {
1036
1595
  mode: 'merge',
1037
1596
  providers: {
1038
1597
  '9router': {
1039
- baseUrl: 'http://9router:20128/v1',
1598
+ baseUrl: nineRouterBase,
1040
1599
  apiKey: 'sk-no-key',
1041
1600
  api: 'openai-completions',
1042
1601
  models: [
@@ -1047,6 +1606,30 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1047
1606
  };
1048
1607
  }
1049
1608
 
1609
+ // Ollama: register provider endpoint so OpenClaw routes ollama/* models correctly
1610
+ if (isLocal) {
1611
+ const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
1612
+ clawConfig.models = {
1613
+ mode: 'merge',
1614
+ providers: {
1615
+ ollama: {
1616
+ baseUrl: 'http://ollama:11434',
1617
+ apiKey: 'ollama-local',
1618
+ models: [
1619
+ { id: 'gemma4:e2b', name: 'Gemma 4 E2B', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
1620
+ { id: 'gemma4:e4b', name: 'Gemma 4 E4B', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
1621
+ { id: 'gemma4:26b', name: 'Gemma 4 26B', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
1622
+ { id: 'gemma4:31b', name: 'Gemma 4 31B', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
1623
+ { id: 'qwen3:8b', name: 'Qwen 3 8B', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
1624
+ { id: 'deepseek-r1:8b', name: 'DeepSeek R1 8B', reasoning: true, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 64000, maxTokens: 8192 },
1625
+ { id: 'llama3.3:8b', name: 'Llama 3.3 8B', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
1626
+ { id: 'gemma3:12b', name: 'Gemma 3 12B', reasoning: false, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192 },
1627
+ ],
1628
+ },
1629
+ },
1630
+ };
1631
+ }
1632
+
1050
1633
  // Browser Automation: inject browser config
1051
1634
  if (hasBrowser) {
1052
1635
  clawConfig.browser = {
@@ -1089,9 +1672,69 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1089
1672
  clawConfig.skills = { entries: skillEntries };
1090
1673
  }
1091
1674
 
1675
+ // Shared multi-bot Telegram runtime: one gateway, multiple accounts + bindings.
1676
+ if (isSharedMultiBot) {
1677
+ const groupId = state.groupId || '';
1678
+ const telegramAccounts = Object.fromEntries(multiBotAgentMetas.map((meta) => [meta.accountId, {
1679
+ botToken: meta.token || '<your_bot_token>',
1680
+ ackReaction: '👍',
1681
+ }]));
1682
+ clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
1683
+ id: meta.agentId,
1684
+ name: meta.name,
1685
+ workspace: `/root/.openclaw/${meta.workspaceDir}`,
1686
+ agentDir: `/root/.openclaw/agents/${meta.agentId}/agent`,
1687
+ model: { primary: state.config.model, fallbacks: [] },
1688
+ }));
1689
+ clawConfig.bindings = multiBotAgentMetas.map((meta) => ({
1690
+ agentId: meta.agentId,
1691
+ match: { channel: 'telegram', accountId: meta.accountId },
1692
+ }));
1693
+ clawConfig.channels.telegram = {
1694
+ enabled: true,
1695
+ defaultAccount: 'default',
1696
+ dmPolicy: 'open',
1697
+ allowFrom: ['*'],
1698
+ groupPolicy: groupId ? 'allowlist' : 'open',
1699
+ groupAllowFrom: ['*'],
1700
+ groups: {
1701
+ [groupId || '*']: {
1702
+ enabled: true,
1703
+ requireMention: false,
1704
+ },
1705
+ },
1706
+ replyToMode: 'first',
1707
+ reactionLevel: 'ack',
1708
+ actions: {
1709
+ sendMessage: true,
1710
+ reactions: true,
1711
+ },
1712
+ accounts: telegramAccounts,
1713
+ };
1714
+ clawConfig.tools = {
1715
+ ...(clawConfig.tools || {}),
1716
+ agentToAgent: {
1717
+ enabled: true,
1718
+ allow: multiBotAgentMetas.map((meta) => meta.agentId),
1719
+ },
1720
+ };
1721
+ clawConfig.plugins = {
1722
+ entries: {
1723
+ 'telegram-multibot-relay': { enabled: true },
1724
+ },
1725
+ };
1726
+ }
1727
+
1092
1728
  setOutput('out-openclaw-json', JSON.stringify(clawConfig, null, 2));
1093
1729
 
1730
+
1094
1731
  // exec-approvals.json — 2-layer fix for OpenClaw exec approval gate
1732
+ const execApprovalsAgents = {
1733
+ main: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true },
1734
+ ...(isSharedMultiBot
1735
+ ? Object.fromEntries(multiBotAgentMetas.map((meta) => [meta.agentId, { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true }]))
1736
+ : { [agentId]: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true } }),
1737
+ };
1095
1738
  const execApprovalsConfig = {
1096
1739
  version: 1,
1097
1740
  defaults: {
@@ -1099,10 +1742,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
1099
1742
  ask: 'off',
1100
1743
  askFallback: 'full'
1101
1744
  },
1102
- agents: {
1103
- main: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true },
1104
- [agentId]: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true }
1105
- }
1745
+ agents: execApprovalsAgents
1106
1746
  };
1107
1747
  setOutput('out-exec-approvals-json', JSON.stringify(execApprovalsConfig, null, 2));
1108
1748
 
@@ -1152,9 +1792,12 @@ model:
1152
1792
  : '';
1153
1793
 
1154
1794
  // Plugins install at runtime (avoids ClawHub rate limit during build)
1155
- const pluginInstallCmd = allPlugins.length > 0
1156
- ? `openclaw plugins install ${allPlugins.join(' ')} 2>/dev/null || true && `
1795
+ const relayPluginInstallCmd = isTelegramMultiBot
1796
+ ? `${buildRelayPluginInstallCommand('openclaw')} && `
1157
1797
  : '';
1798
+ const pluginInstallCmd = allPlugins.length > 0
1799
+ ? `openclaw plugins install ${allPlugins.join(' ')} 2>/dev/null || true && ${relayPluginInstallCmd}`
1800
+ : relayPluginInstallCmd;
1158
1801
  const gatewayCmd = 'openclaw gateway run';
1159
1802
  const browserPrefix = hasBrowser
1160
1803
  ? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
@@ -1172,6 +1815,7 @@ RUN apt-get update && apt-get install -y git curl${browserAptExtra} && rm -rf /v
1172
1815
 
1173
1816
  ARG CACHEBUST=${Date.now()}
1174
1817
  RUN npm install -g openclaw@latest${skillLines}${browserInstallLines}
1818
+ RUN node -e "const fs=require('fs');const p='/usr/local/lib/node_modules/openclaw/dist/gateway-cli-CWpalJNJ.js';let s=fs.readFileSync(p,'utf8');const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';if(!s.includes(to)){if(!s.includes(from)) throw new Error('chat.send patch anchor not found');s=s.replace(from,to);fs.writeFileSync(p,s);}"
1175
1819
  WORKDIR /root/.openclaw
1176
1820
 
1177
1821
  EXPOSE 18791
@@ -1180,21 +1824,26 @@ ${finalCmd}`;
1180
1824
 
1181
1825
  setOutput('out-dockerfile', dockerfile);
1182
1826
 
1827
+ const isMultiBotWizard = state.botCount > 1 && state.channel === 'telegram';
1828
+
1183
1829
  // 4. docker-compose.yml
1184
1830
  // extra_hosts always needed for browser (socat → host Chrome)
1185
1831
  const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
1186
1832
 
1187
1833
  // ─── Dynamic Smart Route Sync Script ────────────────────────────────────────
1188
1834
  // Background loop inside 9Router container every 30s.
1189
- // Queries /api/providers filters connected+enabled updates smart-route combo.
1190
- const syncScript = `const fs=require('fs');const ROUTER='http://localhost:20128';const INTERVAL=30000;const p='/root/.9router/db.json';
1191
- const PM={codex:['cx/gpt-5.4','cx/gpt-5.3-codex','cx/gpt-5.3-codex-high','cx/gpt-5.2-codex','cx/gpt-5.2','cx/gpt-5.1-codex-max','cx/gpt-5.1-codex','cx/gpt-5.1','cx/gpt-5-codex'],'claude-code':['cc/claude-opus-4-6','cc/claude-sonnet-4-6','cc/claude-opus-4-5-20251101','cc/claude-sonnet-4-5-20250929','cc/claude-haiku-4-5-20251001'],github:['gh/gpt-5.4','gh/gpt-5.3-codex','gh/gpt-5.2-codex','gh/gpt-5.2','gh/gpt-5.1-codex-max','gh/gpt-5.1-codex','gh/gpt-5.1','gh/gpt-5','gh/gpt-4.1','gh/gpt-4o','gh/claude-opus-4.6','gh/claude-sonnet-4.6','gh/claude-sonnet-4.5','gh/claude-opus-4.5','gh/claude-haiku-4.5','gh/gemini-3-pro-preview','gh/gemini-3-flash-preview','gh/gemini-2.5-pro'],cursor:['cu/default','cu/claude-4.6-opus-max','cu/claude-4.5-opus-high-thinking','cu/claude-4.5-sonnet-thinking','cu/claude-4.5-sonnet','cu/gpt-5.3-codex','cu/gpt-5.2-codex','cu/gemini-3-flash-preview'],kilo:['kc/anthropic/claude-sonnet-4-20250514','kc/anthropic/claude-opus-4-20250514','kc/google/gemini-2.5-pro','kc/google/gemini-2.5-flash','kc/openai/gpt-4.1','kc/deepseek/deepseek-chat'],cline:['cl/anthropic/claude-sonnet-4.6','cl/anthropic/claude-opus-4.6','cl/openai/gpt-5.3-codex','cl/openai/gpt-5.4','cl/google/gemini-3.1-pro-preview'],'gemini-cli':['gc/gemini-3-flash-preview','gc/gemini-3-pro-preview'],iflow:['if/qwen3-coder-plus','if/kimi-k2','if/kimi-k2-thinking','if/glm-4.7','if/deepseek-r1','if/deepseek-v3.2','if/deepseek-v3','if/qwen3-max','if/qwen3-235b','if/iflow-rome-30ba3b'],qwen:['qw/qwen3-coder-plus','qw/qwen3-coder-flash','qw/vision-model','qw/coder-model'],kiro:['kr/claude-sonnet-4.5','kr/claude-haiku-4.5','kr/deepseek-3.2','kr/deepseek-3.1','kr/qwen3-coder-next'],ollama:['ollama/qwen3.5','ollama/kimi-k2.5','ollama/glm-5','ollama/glm-4.7-flash','ollama/minimax-m2.5','ollama/gpt-oss:120b'],'kimi-coding':['kmc/kimi-k2.5','kmc/kimi-k2.5-thinking','kmc/kimi-latest'],glm:['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],'glm-cn':['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],minimax:['minimax/MiniMax-M2.7','minimax/MiniMax-M2.5','minimax/MiniMax-M2.1'],kimi:['kimi/kimi-k2.5','kimi/kimi-k2.5-thinking','kimi/kimi-latest'],deepseek:['deepseek/deepseek-chat','deepseek/deepseek-reasoner'],xai:['xai/grok-4','xai/grok-4-fast-reasoning','xai/grok-code-fast-1'],mistral:['mistral/mistral-large-latest','mistral/codestral-latest'],groq:['groq/llama-3.3-70b-versatile','groq/openai/gpt-oss-120b'],cerebras:['cerebras/gpt-oss-120b'],alicode:['alicode/qwen3.5-plus','alicode/qwen3-coder-plus'],openai:['openai/gpt-4o','openai/gpt-4.1'],anthropic:['anthropic/claude-sonnet-4','anthropic/claude-haiku-3.5'],gemini:['gemini/gemini-2.5-flash','gemini/gemini-2.5-pro']};
1835
+ // Read providerConnections directly from db.json so smart-route survives
1836
+ // dashboard auth/response changes in newer 9Router builds.
1837
+ const syncScript = `const fs=require('fs');const INTERVAL=30000;const p='/root/.9router/db.json';
1838
+ const PM={codex:['cx/gpt-5.4','cx/gpt-5.3-codex','cx/gpt-5.3-codex-high','cx/gpt-5.2-codex','cx/gpt-5.2','cx/gpt-5.1-codex-max','cx/gpt-5.1-codex','cx/gpt-5.1','cx/gpt-5-codex'],'claude-code':['cc/claude-opus-4-6','cc/claude-sonnet-4-6','cc/claude-opus-4-5-20251101','cc/claude-sonnet-4-5-20250929','cc/claude-haiku-4-5-20251001'],github:['gh/gpt-5.4','gh/gpt-5.3-codex','gh/gpt-5.2-codex','gh/gpt-5.2','gh/gpt-5.1-codex-max','gh/gpt-5.1-codex','gh/gpt-5.1','gh/gpt-5','gh/gpt-4.1','gh/gpt-4o','gh/claude-opus-4.6','gh/claude-sonnet-4.6','gh/claude-sonnet-4.5','gh/claude-opus-4.5','gh/claude-haiku-4.5','gh/gemini-3-pro-preview','gh/gemini-3-flash-preview','gh/gemini-2.5-pro'],cursor:['cu/default','cu/claude-4.6-opus-max','cu/claude-4.5-opus-high-thinking','cu/claude-4.5-sonnet-thinking','cu/claude-4.5-sonnet','cu/gpt-5.3-codex','cu/gpt-5.2-codex','cu/gemini-3-flash-preview'],kilo:['kc/anthropic/claude-sonnet-4-20250514','kc/anthropic/claude-opus-4-20250514','kc/google/gemini-2.5-pro','kc/google/gemini-2.5-flash','kc/openai/gpt-4.1','kc/deepseek/deepseek-chat'],cline:['cl/anthropic/claude-sonnet-4.6','cl/anthropic/claude-opus-4.6','cl/openai/gpt-5.3-codex','cl/openai/gpt-5.4','cl/google/gemini-3.1-pro-preview'],'gemini-cli':['gc/gemini-3-flash-preview','gc/gemini-3-pro-preview'],iflow:['if/qwen3-coder-plus','if/kimi-k2','if/kimi-k2-thinking','if/glm-4.7','if/deepseek-r1','if/deepseek-v3.2','if/deepseek-v3','if/qwen3-max','if/qwen3-235b','if/iflow-rome-30ba3b'],qwen:['qw/qwen3-coder-plus','qw/qwen3-coder-flash','qw/vision-model','qw/coder-model'],kiro:['kr/claude-sonnet-4.5','kr/claude-haiku-4.5','kr/deepseek-3.2','kr/deepseek-3.1','kr/qwen3-coder-next'],ollama:['ollama/gemma4:e2b','ollama/gemma4:e4b','ollama/gemma4:26b','ollama/gemma4:31b','ollama/qwen3.5','ollama/kimi-k2.5','ollama/glm-5','ollama/glm-4.7-flash','ollama/minimax-m2.5','ollama/gpt-oss:120b'],'kimi-coding':['kmc/kimi-k2.5','kmc/kimi-k2.5-thinking','kmc/kimi-latest'],glm:['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],'glm-cn':['glm/glm-5.1','glm/glm-5','glm/glm-4.7'],minimax:['minimax/MiniMax-M2.7','minimax/MiniMax-M2.5','minimax/MiniMax-M2.1'],kimi:['kimi/kimi-k2.5','kimi/kimi-k2.5-thinking','kimi/kimi-latest'],deepseek:['deepseek/deepseek-chat','deepseek/deepseek-reasoner'],xai:['xai/grok-4','xai/grok-4-fast-reasoning','xai/grok-code-fast-1'],mistral:['mistral/mistral-large-latest','mistral/codestral-latest'],groq:['groq/llama-3.3-70b-versatile','groq/openai/gpt-oss-120b'],cerebras:['cerebras/gpt-oss-120b'],alicode:['alicode/qwen3.5-plus','alicode/qwen3-coder-plus'],openai:['openai/gpt-4o','openai/gpt-4.1'],anthropic:['anthropic/claude-sonnet-4','anthropic/claude-haiku-3.5'],gemini:['gemini/gemini-2.5-flash','gemini/gemini-2.5-pro']};
1192
1839
  console.log('[sync-combo] 9Router sync loop started...');
1193
1840
  const sync = async () => {
1194
1841
  try {
1195
- const res = await fetch(ROUTER + '/api/providers');
1196
- const d = await res.json();
1197
- const a = (d.connections || []).filter(c=>(c.isActive !== false && !c.disabled) && (c.isActive || c.connected > 0 || c.tokens?.length > 0)).map(c=>c.provider);
1842
+ let db = {};
1843
+ try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
1844
+ const a = (db.providerConnections || [])
1845
+ .filter(c => c && c.provider && c.isActive !== false && !c.disabled)
1846
+ .map(c => c.provider);
1198
1847
  if (!a.length) return;
1199
1848
 
1200
1849
  const PREF = ['openai','anthropic','claude-code','codex','cursor','github','cline','kimi','minimax','deepseek','glm','alicode','xai','mistral','kilo','kiro','iflow','qwen','gemini-cli','ollama'];
@@ -1202,8 +1851,6 @@ const sync = async () => {
1202
1851
 
1203
1852
  const m = a.flatMap(p => PM[p] || []);
1204
1853
  if (!m.length) return;
1205
- let db = {};
1206
- try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
1207
1854
  if (!db.combos) db.combos = [];
1208
1855
 
1209
1856
  const c = { id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m };
@@ -1225,26 +1872,31 @@ sync();
1225
1872
  setInterval(sync, INTERVAL);`;
1226
1873
 
1227
1874
  let compose;
1228
- if (is9Router) {
1229
- compose = `name: oc-bot
1875
+ if (isMultiBotWizard) {
1876
+ const dependsOn = is9Router
1877
+ ? ' depends_on:\n - 9router\n'
1878
+ : isLocal
1879
+ ? ' depends_on:\n ollama:\n condition: service_healthy\n'
1880
+ : '';
1881
+ const extraHosts = hasBrowser ? `${extraHostsBlock}\n` : '';
1882
+
1883
+ if (is9Router) {
1884
+ compose = `name: oc-multibot
1230
1885
  services:
1231
1886
  ai-bot:
1232
1887
  build: .
1233
- container_name: openclaw-bot
1888
+ container_name: openclaw-multibot
1234
1889
  restart: always
1235
1890
  env_file:
1236
1891
  - .env
1237
- depends_on:
1238
- - 9router
1239
- ${extraHostsBlock}
1240
- volumes:
1892
+ ${dependsOn}${extraHosts} volumes:
1241
1893
  - ../../.openclaw:/root/.openclaw
1242
1894
  ports:
1243
1895
  - "18791:18791"
1244
1896
 
1245
1897
  9router:
1246
1898
  image: node:22-slim
1247
- container_name: 9router
1899
+ container_name: 9router-multibot
1248
1900
  restart: always
1249
1901
  entrypoint:
1250
1902
  - /bin/sh
@@ -1267,7 +1919,63 @@ ${extraHostsBlock}
1267
1919
 
1268
1920
  volumes:
1269
1921
  9router-data:`;
1270
- } else {
1922
+ } else if (isLocal) {
1923
+ const selectedModelId = state.config.model || 'ollama/gemma4:e2b';
1924
+ const ollamaModelTag = selectedModelId.replace('ollama/', '');
1925
+ compose = `name: oc-multibot
1926
+ services:
1927
+ ai-bot:
1928
+ build: .
1929
+ container_name: openclaw-multibot
1930
+ restart: always
1931
+ env_file:
1932
+ - .env
1933
+ ${dependsOn}${extraHosts} volumes:
1934
+ - ../../.openclaw:/root/.openclaw
1935
+ ports:
1936
+ - "18791:18791"
1937
+
1938
+ ollama:
1939
+ image: ollama/ollama:latest
1940
+ container_name: ollama-multibot
1941
+ restart: always
1942
+ environment:
1943
+ - OLLAMA_KEEP_ALIVE=24h
1944
+ - OLLAMA_NUM_PARALLEL=1
1945
+ volumes:
1946
+ - ollama-data:/root/.ollama
1947
+ entrypoint:
1948
+ - /bin/sh
1949
+ - -c
1950
+ - |
1951
+ ollama serve &
1952
+ until ollama list > /dev/null 2>&1; do sleep 1; done
1953
+ ollama pull ${ollamaModelTag}
1954
+ wait
1955
+ healthcheck:
1956
+ test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
1957
+ interval: 10s
1958
+ timeout: 5s
1959
+ retries: 10
1960
+ start_period: 30s
1961
+
1962
+ volumes:
1963
+ ollama-data:`;
1964
+ } else {
1965
+ compose = `name: oc-multibot
1966
+ services:
1967
+ ai-bot:
1968
+ build: .
1969
+ container_name: openclaw-multibot
1970
+ restart: always
1971
+ env_file:
1972
+ - .env
1973
+ ${extraHosts} volumes:
1974
+ - ../../.openclaw:/root/.openclaw
1975
+ ports:
1976
+ - "18791:18791"`;
1977
+ }
1978
+ } else if (is9Router) {
1271
1979
  compose = `name: oc-bot
1272
1980
  services:
1273
1981
  ai-bot:
@@ -1276,14 +1984,103 @@ services:
1276
1984
  restart: always
1277
1985
  env_file:
1278
1986
  - .env
1987
+ depends_on:
1988
+ - 9router
1279
1989
  ${extraHostsBlock}
1280
1990
  volumes:
1281
1991
  - ../../.openclaw:/root/.openclaw
1282
1992
  ports:
1283
- - "18791:18791"`;
1284
- }
1285
-
1286
- setOutput('out-compose', compose);
1993
+ - "18791:18791"
1994
+
1995
+ 9router:
1996
+ image: node:22-slim
1997
+ container_name: 9router
1998
+ restart: always
1999
+ entrypoint:
2000
+ - /bin/sh
2001
+ - -c
2002
+ - |
2003
+ npm install -g 9router
2004
+ cat << 'CLAWEOF' > /tmp/sync.js
2005
+ ${syncScript.replace(/\$/g, '$$$$').replace(/\n/g, '\n ')}
2006
+ CLAWEOF
2007
+ node /tmp/sync.js > /tmp/sync.log 2>&1 &
2008
+ exec 9router -n -t -l -H 0.0.0.0 -p 20128 --skip-update
2009
+ environment:
2010
+ - PORT=20128
2011
+ - HOSTNAME=0.0.0.0
2012
+ - CI=true
2013
+ volumes:
2014
+ - 9router-data:/root/.9router
2015
+ ports:
2016
+ - "20128:20128"
2017
+
2018
+ volumes:
2019
+ 9router-data:`;
2020
+ } else if (isLocal) {
2021
+ // Ollama sidecar — model is pulled automatically on first run
2022
+ const selectedModelId = state.config.model || 'ollama/gemma4:e2b';
2023
+ const ollamaModelTag = selectedModelId.replace('ollama/', '');
2024
+ compose = `name: oc-bot
2025
+ services:
2026
+ ai-bot:
2027
+ build: .
2028
+ container_name: openclaw-bot
2029
+ restart: always
2030
+ env_file: .env
2031
+ depends_on:
2032
+ ollama:
2033
+ condition: service_healthy
2034
+ ${hasBrowser ? extraHostsBlock + '\n' : ''} ports:
2035
+ - "18791:18791"
2036
+ volumes:
2037
+ - ../../.openclaw:/root/.openclaw
2038
+
2039
+ ollama:
2040
+ image: ollama/ollama:latest
2041
+ container_name: ollama
2042
+ restart: always
2043
+ environment:
2044
+ - OLLAMA_KEEP_ALIVE=24h
2045
+ - OLLAMA_NUM_PARALLEL=1
2046
+ # Port NOT exposed to host. Bot connects via Docker network (http://ollama:11434).
2047
+ # Safe even if you already have Ollama installed on this machine.
2048
+ volumes:
2049
+ - ollama-data:/root/.ollama
2050
+ entrypoint:
2051
+ - /bin/sh
2052
+ - -c
2053
+ - |
2054
+ ollama serve &
2055
+ until ollama list > /dev/null 2>&1; do sleep 1; done
2056
+ ollama pull ${ollamaModelTag}
2057
+ wait
2058
+ healthcheck:
2059
+ test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
2060
+ interval: 10s
2061
+ timeout: 5s
2062
+ retries: 10
2063
+ start_period: 30s
2064
+
2065
+ volumes:
2066
+ ollama-data:`;
2067
+ } else {
2068
+ compose = `name: oc-bot
2069
+ services:
2070
+ ai-bot:
2071
+ build: .
2072
+ container_name: openclaw-bot
2073
+ restart: always
2074
+ env_file:
2075
+ - .env
2076
+ ${extraHostsBlock}
2077
+ volumes:
2078
+ - ../../.openclaw:/root/.openclaw
2079
+ ports:
2080
+ - "18791:18791"`;
2081
+ }
2082
+
2083
+ setOutput('out-compose', compose);
1287
2084
 
1288
2085
  // 5. Docker commands
1289
2086
  const approveNote = (document.getElementById('cfg-language')?.value || 'vi') === 'vi'
@@ -1308,35 +2105,55 @@ docker logs -f openclaw-bot${approveNote}`);
1308
2105
 
1309
2106
 
1310
2107
  // 6. Generate auth-profiles.json (root + agent level)
1311
- // Bot and 9Router share Docker network — always use 'sk-no-key'
1312
- const authProviderName = is9Router ? '9router' : state.config.provider;
1313
- const authProfileId = is9Router ? '9router-proxy' : `${authProviderName}:default`;
1314
- const authKeyValue = is9Router
1315
- ? 'sk-no-key'
1316
- : `<your_${(provider.envKey || 'API_KEY').toLowerCase()}>`;
1317
-
1318
- const authProfilesJson = {
1319
- version: 1,
1320
- profiles: {
1321
- [authProfileId]: {
1322
- provider: authProviderName,
1323
- type: 'api_key',
1324
- key: authKeyValue,
2108
+ let authProfilesJson;
2109
+ if (isLocal) {
2110
+ // Ollama: register provider with sidecar URL + any non-empty key
2111
+ authProfilesJson = {
2112
+ version: 1,
2113
+ profiles: {
2114
+ 'ollama:default': {
2115
+ provider: 'ollama',
2116
+ type: 'api_key',
2117
+ key: 'ollama-local',
2118
+ url: 'http://ollama:11434',
2119
+ },
1325
2120
  },
1326
- },
1327
- order: {
1328
- [authProviderName]: [authProfileId],
1329
- },
1330
- };
2121
+ order: { ollama: ['ollama:default'] },
2122
+ };
2123
+ } else {
2124
+ const authProviderName = is9Router ? '9router' : state.config.provider;
2125
+ const authProfileId = is9Router ? '9router-proxy' : `${authProviderName}:default`;
2126
+ const authKeyValue = is9Router
2127
+ ? 'sk-no-key'
2128
+ : `<your_${(provider.envKey || 'API_KEY').toLowerCase()}>`;
2129
+
2130
+ authProfilesJson = {
2131
+ version: 1,
2132
+ profiles: {
2133
+ [authProfileId]: {
2134
+ provider: authProviderName,
2135
+ type: 'api_key',
2136
+ key: authKeyValue,
2137
+ },
2138
+ },
2139
+ order: {
2140
+ [authProviderName]: [authProfileId],
2141
+ },
2142
+ };
2143
+ }
1331
2144
  const authProfilesStr = JSON.stringify(authProfilesJson, null, 2);
1332
2145
 
1333
2146
  // 7. Generate ALL workspace Markdown files
1334
2147
  // OpenClaw auto-injects these into agent context at the start of every session.
1335
2148
  // Hierarchy: per-agent files → global workspace files → config defaults.
1336
- const botName = state.config.botName || 'Chat Bot';
2149
+ const botName = isMultiBotWizard
2150
+ ? (state.bots[0]?.name || state.config.botName || 'Chat Bot')
2151
+ : (state.config.botName || 'Chat Bot');
1337
2152
  const lang = state.config.language || 'vi';
1338
2153
  const userPrompt = state.config.systemPrompt || '';
1339
- const descText = state.config.description || (lang === 'vi' ? 'Trợ lý AI cá nhân' : 'Personal AI assistant');
2154
+ const descText = isMultiBotWizard
2155
+ ? (state.bots[0]?.desc || state.config.description || (lang === 'vi' ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'))
2156
+ : (state.config.description || (lang === 'vi' ? 'Trợ lý AI cá nhân' : 'Personal AI assistant'));
1340
2157
 
1341
2158
  const botEmoji = state.config.emoji || '🤖';
1342
2159
 
@@ -1777,29 +2594,158 @@ fi
1777
2594
  `;
1778
2595
 
1779
2596
  // Store generated files for download
1780
- state._generatedFiles = {
1781
- '.openclaw/openclaw.json': JSON.stringify(clawConfig, null, 2),
1782
- '.openclaw/exec-approvals.json': JSON.stringify(execApprovalsConfig, null, 2),
1783
- '.openclaw/auth-profiles.json': authProfilesStr,
1784
- [`.openclaw/agents/${agentId}.yaml`]: agentYaml,
1785
- [`.openclaw/agents/${agentId}/agent/auth-profiles.json`]: authProfilesStr,
1786
- '.openclaw/workspace/IDENTITY.md': identityMd,
1787
- '.openclaw/workspace/SOUL.md': soulMd,
1788
- '.openclaw/workspace/AGENTS.md': agentsMd,
1789
- '.openclaw/workspace/USER.md': userMd,
1790
- '.openclaw/workspace/TOOLS.md': toolsMd,
1791
- '.openclaw/workspace/MEMORY.md': memoryMd,
1792
- 'docker/openclaw/Dockerfile': dockerfile,
1793
- 'docker/openclaw/docker-compose.yml': compose,
1794
- 'docker/openclaw/.env': document.getElementById('env-content')?.textContent || '',
1795
- '.gitignore': 'docker/openclaw/.env\nnode_modules/',
1796
- ...(hasBrowser ? {
1797
- '.openclaw/workspace/browser-tool.js': browserToolJs,
1798
- '.openclaw/workspace/BROWSER.md': browserMd,
1799
- 'start-chrome-debug.bat': chromeBatContent,
1800
- 'start-chrome-debug.sh': chromeShContent,
1801
- } : {}),
1802
- };
2597
+ if (isMultiBotWizard) {
2598
+ const generatedFiles = {
2599
+ 'docker/openclaw/Dockerfile': dockerfile,
2600
+ 'docker/openclaw/docker-compose.yml': compose,
2601
+ '.gitignore': 'bot*/.env\nnode_modules/',
2602
+ };
2603
+ for (let i = 0; i < state.botCount; i++) {
2604
+ const bot = state.bots[i] || {};
2605
+ const botName = bot.name || `Bot ${i + 1}`;
2606
+ const botDesc = bot.desc || state.config.description || (lang === 'vi' ? 'Trợ lý AI cá nhân' : 'Personal AI assistant');
2607
+ const botAgentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `bot-${i + 1}`;
2608
+ const ownAliases = [botName, bot.slashCmd || '', `bot ${i + 1}`].filter(Boolean);
2609
+ const otherBotNames = state.bots.slice(0, state.botCount).filter((_, idx) => idx !== i).map((peer, idx) => peer?.name || `Bot ${idx + 1}`);
2610
+ const botConfig = JSON.parse(JSON.stringify(clawConfig));
2611
+ botConfig.agents.defaults.model = { primary: state.config.model, fallbacks: [] };
2612
+ botConfig.agents.list = [{
2613
+ id: botAgentId,
2614
+ model: { primary: state.config.model, fallbacks: [] },
2615
+ }];
2616
+ botConfig.gateway = {
2617
+ ...(botConfig.gateway || {}),
2618
+ port: 18791,
2619
+ mode: 'local',
2620
+ bind: '0.0.0.0',
2621
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
2622
+ };
2623
+
2624
+ const botAgentYaml = `name: ${botAgentId}
2625
+ description: "${botDesc}"
2626
+
2627
+ model:
2628
+ primary: ${state.config.model}`;
2629
+ const botIdentityMd = lang === 'vi'
2630
+ ? `# Danh tính
2631
+
2632
+ - **Tên:** ${botName}
2633
+ - **Vai trò:** ${botDesc}
2634
+ - **Emoji:** ${botEmoji}
2635
+
2636
+ ---
2637
+
2638
+ Mình là **${botName}**. Khi ai hỏi tên, mình trả lời: _"Mình là ${botName}"_.`
2639
+ : `# Identity
2640
+
2641
+ - **Name:** ${botName}
2642
+ - **Role:** ${botDesc}
2643
+ - **Emoji:** ${botEmoji}
2644
+
2645
+ ---
2646
+
2647
+ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
2648
+
2649
+ generatedFiles[`bot${i + 1}/.env`] = `TELEGRAM_BOT_TOKEN=${(bot.token || '').trim() || '<your_bot_token>'}${state.groupId ? `\nTELEGRAM_GROUP_ID=${state.groupId}` : ''}\n`;
2650
+ generatedFiles[`bot${i + 1}/.openclaw/openclaw.json`] = JSON.stringify(botConfig, null, 2);
2651
+ generatedFiles[`bot${i + 1}/.openclaw/exec-approvals.json`] = JSON.stringify(execApprovalsConfig, null, 2);
2652
+ generatedFiles[`bot${i + 1}/.openclaw/auth-profiles.json`] = authProfilesStr;
2653
+ generatedFiles[`bot${i + 1}/.openclaw/agents/${botAgentId}.yaml`] = botAgentYaml;
2654
+ generatedFiles[`bot${i + 1}/.openclaw/agents/${botAgentId}/agent/auth-profiles.json`] = authProfilesStr;
2655
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/IDENTITY.md`] = botIdentityMd;
2656
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/SOUL.md`] = soulMd;
2657
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/AGENTS.md`] = agentsMd + (lang === 'vi'
2658
+ ? `\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.`
2659
+ : `\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.`);
2660
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/TEAM.md`] = (lang === 'vi'
2661
+ ? `# Doi Bot\n\n${state.bots.slice(0, state.botCount).map((peer, idx) => `## ${peer?.name || `Bot ${idx + 1}`}\n- Vai tro: ${peer?.desc || state.config.description || 'Tro ly AI ca nhan'}\n- Slash command: ${peer?.slashCmd || '_(chua co)_'}\n- Tinh cach: ${peer?.persona || '_(khong ghi ro)_'}`).join('\n\n')}\n\n## Quy uoc phoi hop\n- Ban biet day du vai tro cua tat ca bot trong doi.\n- Khi user hoi bot nao lam gi, dung file nay lam nguon su that.\n- Neu user dang goi ro bot khac thi khong cuop loi.`
2662
+ : `# Bot Team\n\n${state.bots.slice(0, state.botCount).map((peer, idx) => `## ${peer?.name || `Bot ${idx + 1}`}\n- Role: ${peer?.desc || state.config.description || 'Personal AI assistant'}\n- Slash command: ${peer?.slashCmd || '_(not set)_'}\n- Persona: ${peer?.persona || '_(not specified)_'}`).join('\n\n')}\n\n## Coordination Rules\n- You know the full role roster of every bot in the team.\n- When the user asks which bot does what, use this file as the source of truth.\n- If the user is clearly calling another bot, do not hijack the turn.`);
2663
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/USER.md`] = userMd;
2664
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/TOOLS.md`] = toolsMd;
2665
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/MEMORY.md`] = memoryMd;
2666
+ if (hasBrowser) {
2667
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/browser-tool.js`] = browserToolJs;
2668
+ generatedFiles[`bot${i + 1}/.openclaw/workspace/BROWSER.md`] = browserMd;
2669
+ }
2670
+ }
2671
+
2672
+ if (hasBrowser) {
2673
+ generatedFiles['start-chrome-debug.bat'] = chromeBatContent;
2674
+ generatedFiles['start-chrome-debug.sh'] = chromeShContent;
2675
+ }
2676
+
2677
+ state._generatedFiles = generatedFiles;
2678
+ if (isSharedMultiBot) {
2679
+ const sharedFiles = {
2680
+ '.openclaw/openclaw.json': JSON.stringify(clawConfig, null, 2),
2681
+ '.openclaw/exec-approvals.json': JSON.stringify(execApprovalsConfig, null, 2),
2682
+ '.openclaw/auth-profiles.json': authProfilesStr,
2683
+ 'docker/openclaw/Dockerfile': dockerfile,
2684
+ 'docker/openclaw/docker-compose.yml': compose,
2685
+ 'docker/openclaw/.env': ((document.getElementById('env-content')?.textContent || '').split('\n').filter((line) => !/^TELEGRAM_(BOT_TOKEN|GROUP_ID)=/.test(line)).join('\n').trim() + '\n'),
2686
+ '.gitignore': 'docker/openclaw/.env\nnode_modules/',
2687
+ };
2688
+ sharedFiles['TELEGRAM-POST-INSTALL.md'] = buildTelegramPostInstallChecklist();
2689
+ const teamMd = (lang === 'vi'
2690
+ ? `# 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.`
2691
+ : `# 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 team 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.`);
2692
+ for (const meta of multiBotAgentMetas) {
2693
+ const ownAliases = [meta.name, meta.slashCmd, `bot ${meta.idx + 1}`].filter(Boolean);
2694
+ const otherBots = multiBotAgentMetas.filter((peer) => peer.agentId !== meta.agentId);
2695
+ const relayTargetNames = otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`';
2696
+ const relayTargetIds = otherBots.length ? otherBots.map((peer) => `\`${peer.agentId}\``).join(', ') : '`agent-khac`';
2697
+ sharedFiles[`.openclaw/agents/${meta.agentId}.yaml`] = `name: ${meta.agentId}\ndescription: "${meta.desc}"\n\nmodel:\n primary: ${state.config.model}`;
2698
+ sharedFiles[`.openclaw/agents/${meta.agentId}/agent/auth-profiles.json`] = authProfilesStr;
2699
+ sharedFiles[`.openclaw/${meta.workspaceDir}/IDENTITY.md`] = (lang === 'vi'
2700
+ ? `# Danh tinh\n\n- **Ten:** ${meta.name}\n- **Vai tro:** ${meta.desc}\n- **Emoji:** ${botEmoji}\n`
2701
+ : `# Identity\n\n- **Name:** ${meta.name}\n- **Role:** ${meta.desc}\n- **Emoji:** ${botEmoji}\n`);
2702
+ sharedFiles[`.openclaw/${meta.workspaceDir}/SOUL.md`] = soulMd;
2703
+ sharedFiles[`.openclaw/${meta.workspaceDir}/AGENTS.md`] = agentsMd + (lang === 'vi'
2704
+ ? `\n\n## Khi nao nen tra loi\n- Trong group, xem user dang goi ban neu tin nhan co alias cua ban: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}\n- Neu user tag username Telegram cua ban thi luon tra loi.\n- Gateway se tu dong tha ack \`👍\` khi nhan message; khong can tu tha them neu ack da hien.\n- Neu user dang goi ro bot khac ${relayTargetNames} thi khong cuop loi.\n- Neu user bao ban hoi hoac xin y kien tu ${otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`'} thi dung agent-to-agent noi bo ngay trong turn hien tai.\n- Neu ban la bot mo loi, chi gui 1 cau mo dau ngan roi handoff ngay. Khong tu noi thay bot dich tru khi handoff that bai ro rang.\n- Khi handoff, phai goi dung agent id ky thuat ${relayTargetIds}, khong dung ten hien thi.\n- Neu ban la bot dich nhan handoff, hay tra loi cong khai ngay trong cung Telegram chat/thread bang chinh account cua minh. Uu tien tra loi co \`[[reply_to_current]]\`; neu can, dung Telegram send/sendMessage action thay vi chi output thuong.\n- Dung \`TEAM.md\` lam nguon su that.`
2705
+ : `\n\n## When To Reply\n- In group chats, treat the message as addressed to you if it contains one of your aliases: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}\n- Always reply when your Telegram username is tagged.\n- The gateway auto-sends the \`👍\` ack reaction on inbound messages; do not duplicate it manually if it already appeared.\n- If the message is clearly calling another bot such as ${relayTargetNames}, do not hijack it.\n- If the user asks you to consult ${otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`another bot`'}, use internal agent-to-agent handoff in the same turn.\n- If you are the caller bot, send only one short opener then hand off immediately. Do not speak for the target bot unless the handoff clearly fails.\n- When handing off, use the exact technical agent id ${relayTargetIds}, not the display name.\n- If you are the target bot receiving a handoff, publish the real answer into the same Telegram chat/thread from your own account. Prefer replying with \`[[reply_to_current]]\`; if needed, use the Telegram send/sendMessage action instead of plain assistant output.\n- Use \`TEAM.md\` as the source of truth.`);
2706
+ sharedFiles[`.openclaw/${meta.workspaceDir}/TEAM.md`] = teamMd;
2707
+ sharedFiles[`.openclaw/${meta.workspaceDir}/RELAY.md`] = (lang === 'vi'
2708
+ ? `# Telegram Relay Playbook\n\n## Muc tieu\n- Cho phep bot mo loi goi bot dich noi bo, sau do bot dich tra loi cong khai bang chinh account cua minh.\n\n## Protocol\n1. Bot mo loi gui 1 cau ngan xac nhan se hoi bot dich.\n2. Bot mo loi handoff noi bo bang dung agent id trong \`TEAM.md\`.\n3. Bot dich tra loi cong khai trong cung chat/thread hien tai.\n4. Neu thay \`[[reply_to_current]]\` hoac Telegram send/sendMessage action kha dung, uu tien dung de bam dung message goc.\n5. Neu handoff that bai ro rang, chi bot mo loi moi duoc fallback tom tat.\n`
2709
+ : `# Telegram Relay Playbook\n\n## Goal\n- Let the caller bot consult the target bot internally, then have the target bot publish the real answer with its own Telegram account.\n\n## Protocol\n1. The caller bot sends one short acknowledgement.\n2. The caller bot hands off internally using the exact agent id from \`TEAM.md\`.\n3. The target bot publishes the real answer into the same chat/thread.\n4. If \`[[reply_to_current]]\` or Telegram send/sendMessage is available, prefer it so the answer attaches to the original user turn.\n5. Only the caller bot may summarize as fallback when the handoff clearly fails.\n`);
2710
+ sharedFiles[`.openclaw/${meta.workspaceDir}/USER.md`] = userMd;
2711
+ sharedFiles[`.openclaw/${meta.workspaceDir}/TOOLS.md`] = `${toolsMd}\n\n${lang === 'vi' ? '## Telegram relay\n- Gateway da bat `ackReaction`, `replyToMode:first`, `actions.sendMessage`, va `actions.reactions`.\n- Khi can relay public bang account cua minh sau internal handoff, uu tien dung outbound Telegram action thay vi output mo ho.' : '## Telegram relay\n- The gateway enables `ackReaction`, `replyToMode:first`, `actions.sendMessage`, and `actions.reactions`.\n- When you need to publish a public relay from your own account after an internal handoff, prefer the Telegram outbound action over an ambiguous plain-text answer.'}`;
2712
+ sharedFiles[`.openclaw/${meta.workspaceDir}/MEMORY.md`] = memoryMd;
2713
+ if (hasBrowser) {
2714
+ sharedFiles[`.openclaw/${meta.workspaceDir}/browser-tool.js`] = browserToolJs;
2715
+ sharedFiles[`.openclaw/${meta.workspaceDir}/BROWSER.md`] = browserMd;
2716
+ }
2717
+ }
2718
+ if (hasBrowser) {
2719
+ sharedFiles['start-chrome-debug.bat'] = chromeBatContent;
2720
+ sharedFiles['start-chrome-debug.sh'] = chromeShContent;
2721
+ }
2722
+ state._generatedFiles = sharedFiles;
2723
+ }
2724
+ } else {
2725
+ state._generatedFiles = {
2726
+ '.openclaw/openclaw.json': JSON.stringify(clawConfig, null, 2),
2727
+ '.openclaw/exec-approvals.json': JSON.stringify(execApprovalsConfig, null, 2),
2728
+ '.openclaw/auth-profiles.json': authProfilesStr,
2729
+ [`.openclaw/agents/${agentId}.yaml`]: agentYaml,
2730
+ [`.openclaw/agents/${agentId}/agent/auth-profiles.json`]: authProfilesStr,
2731
+ '.openclaw/workspace/IDENTITY.md': identityMd,
2732
+ '.openclaw/workspace/SOUL.md': soulMd,
2733
+ '.openclaw/workspace/AGENTS.md': agentsMd,
2734
+ '.openclaw/workspace/USER.md': userMd,
2735
+ '.openclaw/workspace/TOOLS.md': toolsMd,
2736
+ '.openclaw/workspace/MEMORY.md': memoryMd,
2737
+ 'docker/openclaw/Dockerfile': dockerfile,
2738
+ 'docker/openclaw/docker-compose.yml': compose,
2739
+ 'docker/openclaw/.env': document.getElementById('env-content')?.textContent || '',
2740
+ '.gitignore': 'docker/openclaw/.env\nnode_modules/',
2741
+ ...(hasBrowser ? {
2742
+ '.openclaw/workspace/browser-tool.js': browserToolJs,
2743
+ '.openclaw/workspace/BROWSER.md': browserMd,
2744
+ 'start-chrome-debug.bat': chromeBatContent,
2745
+ 'start-chrome-debug.sh': chromeShContent,
2746
+ } : {}),
2747
+ };
2748
+ }
1803
2749
 
1804
2750
  // Generate setup bash script
1805
2751
  const setupScript = generateSetupScript(state._generatedFiles);
@@ -1809,11 +2755,839 @@ fi
1809
2755
  const envFinal = document.getElementById('out-env-final');
1810
2756
  const envContent = document.getElementById('env-content');
1811
2757
  if (envFinal && envContent) envFinal.textContent = envContent.textContent;
2758
+
2759
+ // Update Docker download button filename to match OS selection
2760
+ if (typeof updateDockerDlLabel === 'function') updateDockerDlLabel();
2761
+
2762
+ // Multi-bot: inject group setup guide in Step 5
2763
+ const multibotNotice = document.getElementById('multibot-output-notice');
2764
+ if (state.botCount > 1 && state.channel === 'telegram') {
2765
+ const lang = document.getElementById('cfg-language')?.value || 'vi';
2766
+ const isVi = lang === 'vi';
2767
+ const botNames = state.bots.slice(0, state.botCount).map((b, i) =>
2768
+ `@${(b.name || `Bot${i+1}`).replace(/\s+/g,'')}`
2769
+ );
2770
+ const slashCmds = state.bots.slice(0, state.botCount)
2771
+ .filter(b => b.slashCmd)
2772
+ .map(b => `<code>${b.slashCmd}</code>`).join(', ');
2773
+
2774
+ if (multibotNotice) {
2775
+ multibotNotice.style.display = '';
2776
+ multibotNotice.innerHTML = `
2777
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
2778
+ <span style="font-size:24px;">🤖</span>
2779
+ <div>
2780
+ <div style="font-weight:700;font-size:15px;">${isVi ? 'Multi-Bot — Hướng dẫn tạo phòng ban' : 'Multi-Bot — Department Room Guide'}</div>
2781
+ <div style="font-size:12px;color:var(--text-muted);">${isVi ? `${state.botCount} bot đã được cấu hình với routing theo mention.` : `${state.botCount} bots configured with mention-based routing.`}</div>
2782
+ </div>
2783
+ </div>
2784
+ <ol style="margin:0;padding-left:20px;font-size:13px;color:var(--text-secondary);line-height:1.9;">
2785
+ <li>${isVi ? 'Trong Telegram, tạo một Group mới (New Group).' : 'In Telegram, create a New Group.'}</li>
2786
+ <li>${isVi ? `Thêm lần lượt các bot vào: <strong>${botNames.join(', ')}</strong>` : `Add each bot to the group: <strong>${botNames.join(', ')}</strong>`}</li>
2787
+ <li>${isVi ? `Bổ nhiệm mỗi bot làm <strong>Admin</strong> (để có quyền react tin nhắn).` : `Promote each bot to <strong>Admin</strong> (needed for emoji reactions).`}</li>
2788
+ <li>${isVi ? `Lấy Group ID bằng cách forward tin nhắn trong group cho <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a> hoặc <a href="https://t.me/JsonDumpBot" target="_blank">@JsonDumpBot</a>.` : `Get Group ID by forwarding a message from the group to <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a> or <a href="https://t.me/JsonDumpBot" target="_blank">@JsonDumpBot</a>.`}</li>
2789
+ <li>${isVi ? `Nếu đã nhập Group ID ở bước trước, wizard sẽ khóa đúng group đó. Nếu để trống, bot sẽ hoạt động theo chế độ mention-only ở mọi group.` : `If you entered a Group ID earlier, the wizard will lock to that group. If left blank, the bots will run in mention-only mode in any group.`}</li>
2790
+ </ol>
2791
+ <div style="margin-top:12px;padding:10px 14px;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.22);border-radius:8px;font-size:12.5px;color:var(--text-secondary);">
2792
+ <strong>${isVi ? '⚠️ Bat buoc sau khi cai:' : '⚠️ Required after install:'}</strong><br>
2793
+ <span style="color:var(--text-muted);">${isVi
2794
+ ? '1. Vào @BotFather → nhập /mybots → chọn bot → Bot Settings → Group Privacy → Turn off (làm cho TỪNG BOT)<br>2. Remove bot khỏi group rồi Add lại nếu bot đã ở trong group<br>3. Xem file hướng dẫn <strong>TELEGRAM-POST-INSTALL.md</strong> trong thư mục cài đặt để biết thêm chi tiết'
2795
+ : '1. Open @BotFather → type /mybots → select bot → Bot Settings → Group Privacy → Turn off (do this for EACH BOT)<br>2. Remove the bot from the group then re-add it if it was already there<br>3. Read the guide file <strong>TELEGRAM-POST-INSTALL.md</strong> in the installation folder for full details'
2796
+ }</span>
2797
+ </div>
2798
+ <div style="margin-top:14px;padding:10px 14px;background:rgba(99,102,241,0.06);border:1px solid rgba(99,102,241,0.2);border-radius:8px;font-size:12.5px;">
2799
+ <strong>${isVi ? 'Cách sử dụng trong group:' : 'How to use in group:'}</strong><br>
2800
+ <span style="color:var(--text-muted);">
2801
+ ${isVi
2802
+ ? `• Không tag → các bot react 👍❤️🔥 nhưng <em>không reply</em><br>
2803
+ • Tag bot: <code>@TênBot câu hỏi</code> → chỉ bot đó trả lời<br>
2804
+ ${slashCmds ? `• Slash command: ${slashCmds} → bot tương ứng nhận và xử lý` : ''}`
2805
+ : `• No mention → bots react 👍❤️🔥 but <em>stay silent</em><br>
2806
+ • Tag bot: <code>@BotName question</code> → only that bot responds<br>
2807
+ ${slashCmds ? `• Slash commands: ${slashCmds} → respective bot handles it` : ''}`}
2808
+ </span>
2809
+ </div>`;
2810
+ }
2811
+ } else if (multibotNotice) {
2812
+ multibotNotice.style.display = 'none';
2813
+ }
1812
2814
  }
1813
2815
 
2816
+ // ========== Generate Native Setup Script ==========
2817
+ function generateNativeScript() {
2818
+ const lang = document.getElementById('cfg-language')?.value || 'vi';
2819
+ const isVi = lang === 'vi';
2820
+ const provider = PROVIDERS[state.config.provider];
2821
+ const ch = CHANNELS[state.channel];
2822
+ const is9Router = !!(provider && provider.isProxy);
2823
+ const isOllama = !!(provider && provider.isLocal);
2824
+ const hasBrowser = state.config.skills.includes('browser');
2825
+ const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
2826
+ const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
2827
+ const projectDir = state.config.projectPath || '.';
2828
+
2829
+ const allPlugins = [];
2830
+ if (ch && ch.pluginInstall) allPlugins.push(ch.pluginInstall);
2831
+ state.config.plugins.forEach(function(pid) {
2832
+ const p = PLUGINS.find((x) => x.id === pid);
2833
+ if (p) allPlugins.push(p.package);
2834
+ });
2835
+ if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
2836
+ const pluginCmd = allPlugins.length > 0 ? ('npm exec openclaw plugins install ' + allPlugins.join(' ')) : '';
2837
+
2838
+ // ─── Shared initializer (provider install) ───────────────────────────────
2839
+ function providerLines(arr, shell) {
2840
+ if (is9Router) {
2841
+ if (shell === 'bat') {
2842
+ arr.push('npm install -g 9router');
2843
+ arr.push('start "9Router" cmd /k "9router"');
2844
+ arr.push('timeout /t 5 /nobreak >nul');
2845
+ } else {
2846
+ arr.push('npm install -g 9router');
2847
+ arr.push('9router &');
2848
+ arr.push('sleep 3');
2849
+ }
2850
+ } else if (isOllama) {
2851
+ if (shell === 'bat') {
2852
+ arr.push('where ollama >nul 2>&1 || (powershell -Command "Invoke-WebRequest -Uri https://ollama.com/download/OllamaSetup.exe -OutFile OllamaSetup.exe" && OllamaSetup.exe && del OllamaSetup.exe)');
2853
+ arr.push('ollama pull ' + selectedModel);
2854
+ } else {
2855
+ arr.push('command -v ollama > /dev/null 2>&1 || curl -fsSL https://ollama.com/install.sh | sh');
2856
+ arr.push('ollama pull ' + selectedModel);
2857
+ }
2858
+ }
2859
+ }
2860
+
2861
+ const multiBotAgentMetas = isMultiBot
2862
+ ? state.bots.slice(0, state.botCount).map((bot, idx) => {
2863
+ const name = bot?.name || `Bot ${idx + 1}`;
2864
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `bot-${idx + 1}`;
2865
+ return {
2866
+ idx,
2867
+ name,
2868
+ desc: bot?.desc || state.config.description || (isVi ? 'Tro ly AI ca nhan' : 'Personal AI assistant'),
2869
+ persona: bot?.persona || '',
2870
+ slashCmd: bot?.slashCmd || '',
2871
+ token: (bot?.token || '').trim(),
2872
+ agentId: slug,
2873
+ accountId: idx === 0 ? 'default' : slug,
2874
+ workspaceDir: `workspace-${slug}`,
2875
+ };
2876
+ })
2877
+ : [];
2878
+
2879
+ function sharedNativeEnvContent() {
2880
+ const lines = [];
2881
+ if (provider.isProxy) {
2882
+ lines.push('# 9Router: no API key needed');
2883
+ } else if (provider.isLocal) {
2884
+ lines.push('OLLAMA_HOST=http://localhost:11434');
2885
+ lines.push('OLLAMA_API_KEY=ollama-local');
2886
+ } else {
2887
+ lines.push(`${provider.envKey}=${(state.config.apiKey || '').trim() || '<your_api_key>'}`);
2888
+ }
2889
+ return lines.join('\n');
2890
+ }
2891
+
2892
+ function sharedNativeAuthProfilesContent() {
2893
+ let authProfilesJson;
2894
+ if (provider.isLocal) {
2895
+ authProfilesJson = {
2896
+ version: 1,
2897
+ profiles: {
2898
+ 'ollama:default': {
2899
+ provider: 'ollama',
2900
+ type: 'api_key',
2901
+ key: 'ollama-local',
2902
+ url: 'http://localhost:11434',
2903
+ },
2904
+ },
2905
+ order: { ollama: ['ollama:default'] },
2906
+ };
2907
+ } else {
2908
+ const authProviderName = provider.isProxy ? '9router' : provider.id;
2909
+ const authProfileId = provider.isProxy ? '9router-proxy' : `${authProviderName}:default`;
2910
+ const authKeyValue = provider.isProxy
2911
+ ? 'sk-no-key'
2912
+ : ((state.config.apiKey || '').trim() || `<your_${(provider.envKey || 'API_KEY').toLowerCase()}>`);
2913
+ authProfilesJson = {
2914
+ version: 1,
2915
+ profiles: {
2916
+ [authProfileId]: {
2917
+ provider: authProviderName,
2918
+ type: 'api_key',
2919
+ key: authKeyValue,
2920
+ },
2921
+ },
2922
+ order: { [authProviderName]: [authProfileId] },
2923
+ };
2924
+ if (!provider.isProxy && provider.baseURL) {
2925
+ authProfilesJson.profiles[authProfileId].url = provider.baseURL;
2926
+ }
2927
+ }
2928
+ return JSON.stringify(authProfilesJson, null, 2);
2929
+ }
1814
2930
 
2931
+ function sharedNativeExecApprovalsContent() {
2932
+ return JSON.stringify({
2933
+ version: 1,
2934
+ defaults: {
2935
+ security: 'full',
2936
+ ask: 'off',
2937
+ askFallback: 'full',
2938
+ },
2939
+ agents: {
2940
+ main: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true },
2941
+ ...Object.fromEntries(multiBotAgentMetas.map((meta) => [meta.agentId, { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true }])),
2942
+ },
2943
+ }, null, 2);
2944
+ }
2945
+
2946
+ function sharedNativeConfigContent() {
2947
+ const groupId = state.groupId || '';
2948
+ const telegramAccounts = Object.fromEntries(multiBotAgentMetas.map((meta) => [meta.accountId, {
2949
+ botToken: meta.token || '<your_bot_token>',
2950
+ ackReaction: '👍',
2951
+ }]));
2952
+ const cfg = {
2953
+ meta: { lastTouchedVersion: '2026.3.24' },
2954
+ agents: {
2955
+ defaults: {
2956
+ model: { primary: state.config.model, fallbacks: [] },
2957
+ compaction: { mode: 'safeguard' },
2958
+ timeoutSeconds: provider.isLocal ? 900 : 120,
2959
+ ...(provider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
2960
+ },
2961
+ list: multiBotAgentMetas.map((meta) => ({
2962
+ id: meta.agentId,
2963
+ name: meta.name,
2964
+ workspace: `./.openclaw/${meta.workspaceDir}`,
2965
+ agentDir: `./.openclaw/agents/${meta.agentId}/agent`,
2966
+ model: { primary: state.config.model, fallbacks: [] },
2967
+ })),
2968
+ },
2969
+ commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
2970
+ bindings: multiBotAgentMetas.map((meta) => ({
2971
+ agentId: meta.agentId,
2972
+ match: { channel: 'telegram', accountId: meta.accountId },
2973
+ })),
2974
+ channels: {
2975
+ telegram: {
2976
+ enabled: true,
2977
+ defaultAccount: 'default',
2978
+ dmPolicy: 'open',
2979
+ allowFrom: ['*'],
2980
+ groupPolicy: groupId ? 'allowlist' : 'open',
2981
+ groupAllowFrom: ['*'],
2982
+ groups: {
2983
+ [groupId || '*']: { enabled: true, requireMention: false },
2984
+ },
2985
+ replyToMode: 'first',
2986
+ reactionLevel: 'ack',
2987
+ actions: {
2988
+ sendMessage: true,
2989
+ reactions: true,
2990
+ },
2991
+ accounts: telegramAccounts,
2992
+ },
2993
+ },
2994
+ tools: {
2995
+ profile: 'full',
2996
+ exec: { host: 'gateway', security: 'full', ask: 'off' },
2997
+ agentToAgent: {
2998
+ enabled: true,
2999
+ allow: multiBotAgentMetas.map((meta) => meta.agentId),
3000
+ },
3001
+ },
3002
+ plugins: {
3003
+ entries: {
3004
+ 'telegram-multibot-relay': { enabled: true },
3005
+ },
3006
+ },
3007
+ gateway: {
3008
+ port: 18791,
3009
+ mode: 'local',
3010
+ bind: '0.0.0.0',
3011
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3012
+ },
3013
+ };
3014
+ return JSON.stringify(cfg, null, 2);
3015
+ }
3016
+
3017
+ function sharedNativeFileMap() {
3018
+ const files = {
3019
+ '.env': sharedNativeEnvContent(),
3020
+ '.openclaw/openclaw.json': sharedNativeConfigContent(),
3021
+ '.openclaw/exec-approvals.json': sharedNativeExecApprovalsContent(),
3022
+ '.openclaw/auth-profiles.json': sharedNativeAuthProfilesContent(),
3023
+ 'TELEGRAM-POST-INSTALL.md': buildTelegramPostInstallChecklist(),
3024
+ };
3025
+ const teamMd = isVi
3026
+ ? `# 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.`
3027
+ : `# 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.`;
3028
+ const userMd = isVi
3029
+ ? `# 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)_'}`
3030
+ : `# User Profile\n\n## Overview\n- **Preferred language:** English\n\n## Notes\n${state.config.userInfo || '- _(Nothing yet)_'}`
3031
+ ;
3032
+ const selectedSkillNames = state.config.skills.map((sid) => {
3033
+ const skill = SKILLS.find((s) => s.id === sid);
3034
+ return skill ? `- **${skill.name}**${skill.slug ? ` (${skill.slug})` : ''}` : null;
3035
+ }).filter(Boolean);
3036
+ const toolsMd = isVi
3037
+ ? `# Huong dan su dung Tools\n\n## Skills da cai\n${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(Chua co skill nao)_'}\n\n## Quy uoc\n- Uu tien dung tool thay vi doan\n- Browser: dung khi user yeu cau thao tac web\n- Memory: cap nhat khi biet thong tin quan trong`
3038
+ : `# 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`;
3039
+ const memoryMd = isVi ? '# Bo nho dai han\n\n## Ghi chu\n- _(Chua co gi)_' : '# Long-term Memory\n\n## Notes\n- _(Nothing yet)_';
3040
+ for (const meta of multiBotAgentMetas) {
3041
+ const ownAliases = [meta.name, meta.slashCmd, `bot ${meta.idx + 1}`].filter(Boolean);
3042
+ const otherBots = multiBotAgentMetas.filter((peer) => peer.agentId !== meta.agentId);
3043
+ const relayTargetNames = otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`';
3044
+ const relayTargetIds = otherBots.length ? otherBots.map((peer) => `\`${peer.agentId}\``).join(', ') : '`agent-khac`';
3045
+ files[`.openclaw/agents/${meta.agentId}.yaml`] = `name: ${meta.agentId}\ndescription: "${meta.desc}"\n\nmodel:\n primary: ${state.config.model}`;
3046
+ files[`.openclaw/agents/${meta.agentId}/agent/auth-profiles.json`] = sharedNativeAuthProfilesContent();
3047
+ files[`.openclaw/${meta.workspaceDir}/IDENTITY.md`] = isVi
3048
+ ? `# Danh tinh\n\n- **Ten:** ${meta.name}\n- **Vai tro:** ${meta.desc}\n\n---\n\nMinh la **${meta.name}**.`
3049
+ : `# Identity\n\n- **Name:** ${meta.name}\n- **Role:** ${meta.desc}\n\n---\n\nI am **${meta.name}**.`;
3050
+ files[`.openclaw/${meta.workspaceDir}/SOUL.md`] = isVi
3051
+ ? `# Tinh cach\n\n${meta.persona || 'Huu ich that su, gan gui, ngan gon.'}`
3052
+ : `# Soul\n\n${meta.persona || 'Helpful, concise, and practical.'}`;
3053
+ files[`.openclaw/${meta.workspaceDir}/AGENTS.md`] = (isVi
3054
+ ? `# Huong dan van hanh\n\n## Vai tro\nBan la **${meta.name}**, ${meta.desc.toLowerCase()}.\n\n## Khi nao nen tra loi\n- Trong group, xem user dang goi ban neu tin nhan co alias cua ban: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}\n- Neu user tag username Telegram cua ban thi luon tra loi.\n- Gateway se tu dong tha ack \`👍\` khi nhan message; khong can tu tha them neu ack da hien.\n- Neu user dang goi ro bot khac ${relayTargetNames} thi khong cuop loi.\n\n## Phoi hop\n- Dung \`TEAM.md\` lam nguon su that.\n- Neu user bao ban hoi hoac xin y kien tu ${otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`'} thi dung agent-to-agent noi bo ngay trong turn hien tai.\n- Neu ban la bot mo loi, chi gui 1 cau mo dau ngan roi handoff ngay. Khong tu noi thay bot dich tru khi handoff that bai ro rang.\n- Khi handoff, phai goi dung agent id ky thuat ${relayTargetIds}, khong dung ten hien thi.\n- Neu ban la bot dich nhan handoff, hay tra loi cong khai ngay trong cung Telegram chat/thread bang chinh account cua minh. Uu tien tra loi co \`[[reply_to_current]]\`; neu can, dung Telegram send/sendMessage action thay vi chi output thuong.\n- Khong bao user phai tag lai bot kia neu ban co the hoi noi bo duoc.`
3055
+ : `# Operating Manual\n\n## Role\nYou are **${meta.name}**, ${meta.desc.toLowerCase()}.\n\n## When To Reply\n- In group chats, treat the message as addressed to you if it contains one of your aliases: ${ownAliases.map((alias) => `\`${alias}\``).join(', ')}\n- Always reply when your Telegram username is tagged.\n- The gateway auto-sends the \`👍\` ack reaction on inbound messages; do not duplicate it manually if it already appeared.\n- If the message is clearly calling another bot such as ${relayTargetNames}, do not hijack it.\n\n## Coordination\n- Use \`TEAM.md\` as the source of truth.\n- If the user asks you to consult ${otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`another bot`'}, use internal agent-to-agent handoff in the same turn.\n- If you are the caller bot, send only one short opener then hand off immediately. Do not speak for the target bot unless the handoff clearly fails.\n- When handing off, use the exact technical agent id ${relayTargetIds}, not the display name.\n- If you are the target bot receiving a handoff, publish the real answer into the same Telegram chat/thread from your own account. Prefer replying with \`[[reply_to_current]]\`; if needed, use the Telegram send/sendMessage action instead of plain assistant output.\n- Do not ask the user to tag the other bot again if you can consult internally.`);
3056
+ files[`.openclaw/${meta.workspaceDir}/TEAM.md`] = teamMd;
3057
+ files[`.openclaw/${meta.workspaceDir}/RELAY.md`] = isVi
3058
+ ? `# Telegram Relay Playbook\n\n## Muc tieu\n- Cho phep bot mo loi goi bot dich noi bo, sau do bot dich tra loi cong khai bang chinh account cua minh.\n\n## Protocol\n1. Bot mo loi gui 1 cau ngan xac nhan se hoi bot dich.\n2. Bot mo loi handoff noi bo bang dung agent id trong \`TEAM.md\`.\n3. Bot dich tra loi cong khai trong cung chat/thread hien tai.\n4. Neu thay \`[[reply_to_current]]\` hoac Telegram send/sendMessage action kha dung, uu tien dung de bam dung message goc.\n5. Neu handoff that bai ro rang, chi bot mo loi moi duoc fallback tom tat.\n`
3059
+ : `# Telegram Relay Playbook\n\n## Goal\n- Let the caller bot consult the target bot internally, then have the target bot publish the real answer with its own Telegram account.\n\n## Protocol\n1. The caller bot sends one short acknowledgement.\n2. The caller bot hands off internally using the exact agent id from \`TEAM.md\`.\n3. The target bot publishes the real answer into the same chat/thread.\n4. If \`[[reply_to_current]]\` or Telegram send/sendMessage is available, prefer it so the answer attaches to the original user turn.\n5. Only the caller bot may summarize as fallback when the handoff clearly fails.\n`;
3060
+ files[`.openclaw/${meta.workspaceDir}/USER.md`] = userMd;
3061
+ files[`.openclaw/${meta.workspaceDir}/TOOLS.md`] = `${toolsMd}\n\n${isVi ? '## Telegram relay\n- Gateway da bat `ackReaction`, `replyToMode:first`, `actions.sendMessage`, va `actions.reactions`.\n- Khi can relay public bang account cua minh sau internal handoff, uu tien dung outbound Telegram action thay vi output mo ho.' : '## Telegram relay\n- The gateway enables `ackReaction`, `replyToMode:first`, `actions.sendMessage`, and `actions.reactions`.\n- When you need to publish a public relay from your own account after an internal handoff, prefer the Telegram outbound action over an ambiguous plain-text answer.'}`;
3062
+ files[`.openclaw/${meta.workspaceDir}/MEMORY.md`] = memoryMd;
3063
+ if (hasBrowser) {
3064
+ files[`.openclaw/${meta.workspaceDir}/browser-tool.js`] = `const { chromium } = require('playwright');\n(async () => {\n const [,, action, param1, param2] = process.argv;\n const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');\n const ctx = browser.contexts()[0] || await browser.newContext();\n const page = ctx.pages()[0] || await ctx.newPage();\n if (action === 'open') await page.goto(param1, { waitUntil: 'domcontentloaded', timeout: 30000 });\n else if (action === 'click') await page.locator(param1).first().click({ timeout: 5000 });\n else if (action === 'fill') await page.locator(param1).first().fill(param2, { timeout: 5000 });\n else if (action === 'press') await page.keyboard.press(param1);\n else console.log(await page.title(), page.url());\n await browser.close();\n})();\n`;
3065
+ files[`.openclaw/${meta.workspaceDir}/BROWSER.md`] = isVi
3066
+ ? '# Browser Automation\n\nDung `browser-tool.js` de dieu khien Chrome debug tai `http://127.0.0.1:9222`.'
3067
+ : '# Browser Automation\n\nUse `browser-tool.js` to control Chrome debug on `http://127.0.0.1:9222`.';
3068
+ }
3069
+ }
3070
+ return files;
3071
+ }
3072
+
3073
+ // ─── Per-bot ENV content ──────────────────────────────────────────────────
3074
+ function botEnvContent(botIndex) {
3075
+ const bot = state.bots[botIndex] || {};
3076
+ const botProvider = PROVIDERS[bot.provider] || provider;
3077
+ const lines = [];
3078
+ if (botProvider.isProxy) {
3079
+ lines.push('# 9Router: no API key needed');
3080
+ } else if (botProvider.isLocal) {
3081
+ lines.push('OLLAMA_HOST=http://localhost:11434');
3082
+ lines.push('OLLAMA_API_KEY=ollama-local');
3083
+ } else {
3084
+ const keyVal = (bot.apiKey || state.config.apiKey || '').trim();
3085
+ lines.push(`${botProvider.envKey}=${keyVal || '<your_api_key>'}`);
3086
+ }
3087
+ const tok = (bot.token || '').trim();
3088
+ lines.push(`TELEGRAM_BOT_TOKEN=${tok || '<your_bot_token>'}`);
3089
+ if (state.groupId) lines.push(`TELEGRAM_GROUP_ID=${state.groupId}`);
3090
+ return lines.join('\n');
3091
+ }
3092
+
3093
+ // ─── Per-bot openclaw.json (minimal — shared workspace) ──────────────────
3094
+ function botConfigContent(botIndex) {
3095
+ const bot = state.bots[botIndex] || {};
3096
+ const botName = bot.name || `Bot ${botIndex + 1}`;
3097
+ const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3098
+ const basePort = 18791 + botIndex;
3099
+ const groupId = state.groupId || '';
3100
+ const channelConfig = JSON.parse(JSON.stringify(ch.channelConfig || {}));
3101
+ if (state.channel === 'telegram' && isMultiBot) {
3102
+ channelConfig.groupPolicy = groupId ? 'allowlist' : 'open';
3103
+ channelConfig.groupAllowFrom = ['*'];
3104
+ channelConfig.groups = {
3105
+ [groupId || '*']: {
3106
+ enabled: true,
3107
+ requireMention: false,
3108
+ },
3109
+ };
3110
+ }
3111
+ const cfg = {
3112
+ meta: { lastTouchedVersion: '2026.3.24' },
3113
+ agents: {
3114
+ defaults: { model: { primary: bot.model || state.config.model }, compaction: { mode: 'safeguard' }, timeoutSeconds: 120 },
3115
+ list: [{ id: agentId, model: { primary: bot.model || state.config.model } }],
3116
+ },
3117
+ commands: { native: 'auto', nativeSkills: 'auto', restart: true },
3118
+ channels: channelConfig,
3119
+ gateway: {
3120
+ port: basePort,
3121
+ mode: 'local',
3122
+ bind: '0.0.0.0',
3123
+ auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3124
+ },
3125
+
3126
+ };
3127
+ return JSON.stringify(cfg, null, 2);
3128
+ }
3129
+
3130
+ function botAuthProfilesContent(botIndex) {
3131
+ const bot = state.bots[botIndex] || {};
3132
+ const botProvider = PROVIDERS[bot.provider] || provider;
3133
+ let authProfilesJson;
3134
+ if (botProvider.isLocal) {
3135
+ authProfilesJson = {
3136
+ version: 1,
3137
+ profiles: {
3138
+ 'ollama:default': {
3139
+ provider: 'ollama',
3140
+ type: 'api_key',
3141
+ key: 'ollama-local',
3142
+ url: 'http://localhost:11434',
3143
+ },
3144
+ },
3145
+ order: { ollama: ['ollama:default'] },
3146
+ };
3147
+ } else {
3148
+ const authProviderName = botProvider.isProxy ? '9router' : botProvider.id;
3149
+ const authProfileId = botProvider.isProxy ? '9router-proxy' : `${authProviderName}:default`;
3150
+ const authKeyValue = botProvider.isProxy
3151
+ ? 'sk-no-key'
3152
+ : ((bot.apiKey || state.config.apiKey || '').trim() || `<your_${(botProvider.envKey || 'API_KEY').toLowerCase()}>`);
3153
+ authProfilesJson = {
3154
+ version: 1,
3155
+ profiles: {
3156
+ [authProfileId]: {
3157
+ provider: authProviderName,
3158
+ type: 'api_key',
3159
+ key: authKeyValue,
3160
+ },
3161
+ },
3162
+ order: { [authProviderName]: [authProfileId] },
3163
+ };
3164
+ if (!botProvider.isProxy && botProvider.baseURL) {
3165
+ authProfilesJson.profiles[authProfileId].url = botProvider.baseURL;
3166
+ }
3167
+ }
3168
+ return JSON.stringify(authProfilesJson, null, 2);
3169
+ }
3170
+
3171
+ function botExecApprovalsContent(botIndex) {
3172
+ const bot = state.bots[botIndex] || {};
3173
+ const botName = bot.name || `Bot ${botIndex + 1}`;
3174
+ const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3175
+ return JSON.stringify({
3176
+ version: 1,
3177
+ defaults: {
3178
+ security: 'full',
3179
+ ask: 'off',
3180
+ askFallback: 'full'
3181
+ },
3182
+ agents: {
3183
+ main: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true },
3184
+ [agentId]: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true }
3185
+ }
3186
+ }, null, 2);
3187
+ }
3188
+
3189
+ function botAgentYamlContent(botIndex) {
3190
+ const bot = state.bots[botIndex] || {};
3191
+ const botName = bot.name || `Bot ${botIndex + 1}`;
3192
+ const botDesc = bot.desc || state.config.description || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant');
3193
+ const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3194
+ return `name: ${agentId}
3195
+ description: "${botDesc}"
3196
+
3197
+ model:
3198
+ primary: ${bot.model || state.config.model}`;
3199
+ }
3200
+
3201
+ function botWorkspaceFiles(botIndex) {
3202
+ const bot = state.bots[botIndex] || {};
3203
+ const botName = bot.name || `Bot ${botIndex + 1}`;
3204
+ const botDesc = bot.desc || state.config.description || (isVi ? 'Trợ lý AI cá nhân' : 'Personal AI assistant');
3205
+ const botPersona = bot.persona || '';
3206
+ const teamRoster = state.bots.slice(0, state.botCount).map((peer, idx) => ({
3207
+ idx,
3208
+ name: peer.name || `Bot ${idx + 1}`,
3209
+ desc: peer.desc || (isVi ? 'Tro ly AI ca nhan' : 'Personal AI assistant'),
3210
+ persona: peer.persona || '',
3211
+ slashCmd: peer.slashCmd || '',
3212
+ }));
3213
+ const ownAliases = [botName, bot.slashCmd || '', `bot ${botIndex + 1}`].filter(Boolean);
3214
+ const otherBotNames = teamRoster.filter((peer) => peer.idx !== botIndex).map((peer) => peer.name);
3215
+ const userInfoText = state.config.userInfo || '';
3216
+ const selectedSkillNames = state.config.skills.map((sid) => {
3217
+ const skill = SKILLS.find((s) => s.id === sid);
3218
+ return skill ? `- **${skill.name}**${skill.slug ? ` (${skill.slug})` : ''}` : null;
3219
+ }).filter(Boolean);
3220
+ const identityMd = isVi
3221
+ ? `# Danh tính
3222
+
3223
+ - **Tên:** ${botName}
3224
+ - **Vai trò:** ${botDesc}
3225
+
3226
+ ---
3227
+
3228
+ Mình là **${botName}**. Khi ai hỏi tên, mình trả lời: _"Mình là ${botName}"_.`
3229
+ : `# Identity
3230
+
3231
+ - **Name:** ${botName}
3232
+ - **Role:** ${botDesc}
3233
+
3234
+ ---
3235
+
3236
+ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
3237
+ const soulMd = isVi
3238
+ ? `# Tính cách
3239
+
3240
+ **Hữu ích thật sự.** Bỏ qua câu nệ, cứ giúp thẳng.
3241
+ **Có cá tính.** Trợ lý không có cá tính thì chỉ là công cụ.
3242
+
3243
+ ## Phong cách
3244
+ - Tự nhiên, gần gũi
3245
+ - Trực tiếp, ngắn gọn
3246
+ ${botPersona ? `\n## Custom Rules\n${botPersona}` : ''}`
3247
+ : `# Soul
3248
+
3249
+ **Be genuinely helpful.** Skip filler and just help.
3250
+ **Have personality.** An assistant with no personality is just a tool.
3251
+
3252
+ ## Style
3253
+ - Natural and concise
3254
+ - Direct and practical
3255
+ ${botPersona ? `\n## Custom Rules\n${botPersona}` : ''}`;
3256
+ const teamMd = isVi
3257
+ ? `# Doi Bot
3258
+
3259
+ ${teamRoster.map((peer) => `## ${peer.name}
3260
+ - Vai tro: ${peer.desc}
3261
+ - Slash command: ${peer.slashCmd || '_(chua co)_'}
3262
+ - Tinh cach: ${peer.persona || '_(khong ghi ro)_'}`).join('\n\n')}
3263
+
3264
+ ## Quy uoc phoi hop
3265
+ - Ban biet day du vai tro cua tat ca bot trong doi.
3266
+ - Khi user hoi bot nao lam gi, dung file nay lam nguon su that.
3267
+ - Neu user dang goi ro bot khac thi khong cuop loi.`
3268
+ : `# Bot Team
3269
+
3270
+ ${teamRoster.map((peer) => `## ${peer.name}
3271
+ - Role: ${peer.desc}
3272
+ - Slash command: ${peer.slashCmd || '_(not set)_'}
3273
+ - Persona: ${peer.persona || '_(not specified)_'}`).join('\n\n')}
3274
+
3275
+ ## Coordination Rules
3276
+ - You know the full role roster of every bot in the team.
3277
+ - When the user asks which bot does what, use this file as the source of truth.
3278
+ - If the user is clearly calling another bot, do not hijack the turn.`;
3279
+ const agentsMd = isVi
3280
+ ? `# Hướng dẫn vận hành
3281
+
3282
+ ## Vai trò
3283
+ Bạn là **${botName}**, ${botDesc.toLowerCase()}.
3284
+
3285
+ ## Quy tắc trả lời
3286
+ - Trả lời ngắn gọn, súc tích
3287
+ - Ưu tiên tiếng Việt
3288
+ - Khi hỏi tên: _"Mình là ${botName}"_
3289
+ - Không bịa thông tin`
3290
+ : `# Operating Manual
3291
+
3292
+ ## Role
3293
+ You are **${botName}**, ${botDesc.toLowerCase()}.
3294
+
3295
+ ## Reply Rules
3296
+ - Be concise
3297
+ - Prefer English unless user uses another language
3298
+ - When asked your name: _"I'm ${botName}"_
3299
+ - Never fabricate information`;
3300
+ const extraAgentsMd = isVi
3301
+ ? `\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.`
3302
+ : `\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.`;
3303
+ const userMd = isVi
3304
+ ? `# Thông tin người dùng
3305
+
3306
+ ## Tổng quan
3307
+ - **Ngôn ngữ ưu tiên:** Tiếng Việt
3308
+
3309
+ ## Thông tin cá nhân
3310
+ ${userInfoText || '- _(Chưa có gì)_'}`
3311
+ : `# User Profile
3312
+
3313
+ ## Overview
3314
+ - **Preferred language:** English
3315
+
3316
+ ## Notes
3317
+ ${userInfoText || '- _(Nothing yet)_'}`
3318
+ ;
3319
+ const toolsMd = isVi
3320
+ ? `# Hướng dẫn sử dụng Tools
3321
+
3322
+ ## Skills đã cài
3323
+ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(Chưa có skill nào)_'}
3324
+
3325
+ ## Quy ước
3326
+ - Ưu tiên dùng tool thay vì đoán
3327
+ - Browser: dùng khi user yêu cầu thao tác web
3328
+ - Memory: cập nhật khi biết thông tin quan trọng`
3329
+ : `# Tool Usage Guide
3330
+
3331
+ ## Installed Skills
3332
+ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills installed)_'}
3333
+
3334
+ ## Conventions
3335
+ - Prefer tools over guessing
3336
+ - Use Browser for explicit web tasks
3337
+ - Update Memory when important user info appears`;
3338
+ const memoryMd = isVi
3339
+ ? `# Bộ nhớ dài hạn
3340
+
3341
+ ## Ghi chú
3342
+ - _(Chưa có gì)_`
3343
+ : `# Long-term Memory
3344
+
3345
+ ## Notes
3346
+ - _(Nothing yet)_`;
3347
+ const files = {
3348
+ 'IDENTITY.md': identityMd,
3349
+ 'SOUL.md': soulMd,
3350
+ 'AGENTS.md': agentsMd + extraAgentsMd,
3351
+ 'TEAM.md': teamMd,
3352
+ 'USER.md': userMd,
3353
+ 'TOOLS.md': toolsMd,
3354
+ 'MEMORY.md': memoryMd,
3355
+ };
3356
+ if (hasBrowser) {
3357
+ files['browser-tool.js'] = `const { chromium } = require('playwright');\n(async () => {\n const [,, action, param1, param2] = process.argv;\n const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');\n const ctx = browser.contexts()[0] || await browser.newContext();\n const page = ctx.pages()[0] || await ctx.newPage();\n if (action === 'open') await page.goto(param1, { waitUntil: 'domcontentloaded', timeout: 30000 });\n else if (action === 'click') await page.locator(param1).first().click({ timeout: 5000 });\n else if (action === 'fill') await page.locator(param1).first().fill(param2, { timeout: 5000 });\n else if (action === 'press') await page.keyboard.press(param1);\n else console.log(await page.title(), page.url());\n await browser.close();\n})();\n`;
3358
+ files['BROWSER.md'] = isVi
3359
+ ? `# Browser Automation\n\nDùng file \`browser-tool.js\` để điều khiển Chrome debug tại \`http://127.0.0.1:9222\`.`
3360
+ : `# Browser Automation\n\nUse \`browser-tool.js\` to control Chrome debug on \`http://127.0.0.1:9222\`.`;
3361
+ }
3362
+ return files;
3363
+ }
3364
+
3365
+ function botFiles(botIndex) {
3366
+ const bot = state.bots[botIndex] || {};
3367
+ const botName = bot.name || `Bot ${botIndex + 1}`;
3368
+ const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3369
+ const base = '.';
3370
+ const files = {};
3371
+ files[`${base}/.env`] = botEnvContent(botIndex);
3372
+ files[`${base}/.openclaw/openclaw.json`] = botConfigContent(botIndex);
3373
+ files[`${base}/.openclaw/exec-approvals.json`] = botExecApprovalsContent(botIndex);
3374
+ files[`${base}/.openclaw/auth-profiles.json`] = botAuthProfilesContent(botIndex);
3375
+ files[`${base}/.openclaw/agents/${agentId}.yaml`] = botAgentYamlContent(botIndex);
3376
+ files[`${base}/.openclaw/agents/${agentId}/agent/auth-profiles.json`] = botAuthProfilesContent(botIndex);
3377
+ Object.entries(botWorkspaceFiles(botIndex)).forEach(([name, content]) => {
3378
+ files[`${base}/.openclaw/workspace/${name}`] = content;
3379
+ });
3380
+ return files;
3381
+ }
3382
+
3383
+ function appendShWriteCommands(arr, files) {
3384
+ Object.entries(files).forEach(([relPath, content]) => {
3385
+ const dir = relPath.substring(0, relPath.lastIndexOf('/'));
3386
+ if (dir) arr.push(`mkdir -p "${dir}"`);
3387
+ arr.push(`cat > "${relPath}" << 'CLAWEOF'\n${content}\nCLAWEOF`);
3388
+ });
3389
+ }
3390
+
3391
+ function batEscapeEchoLine(line) {
3392
+ return line
3393
+ .replace(/\^/g, '^^')
3394
+ .replace(/&/g, '^&')
3395
+ .replace(/\|/g, '^|')
3396
+ .replace(/</g, '^<')
3397
+ .replace(/>/g, '^>')
3398
+ .replace(/\(/g, '^(')
3399
+ .replace(/\)/g, '^)')
3400
+ .replace(/%/g, '%%');
3401
+ }
3402
+
3403
+ function appendBatWriteCommands(arr, files) {
3404
+ Object.entries(files).forEach(([relPath, content]) => {
3405
+ const winPath = relPath.replace(/\//g, '\\');
3406
+ const dir = winPath.substring(0, winPath.lastIndexOf('\\'));
3407
+ if (dir) arr.push(`if not exist "${dir}" mkdir "${dir}"`);
3408
+ arr.push(`> "${winPath}" (`);
3409
+ content.split('\n').forEach((line) => {
3410
+ arr.push(line.length ? `echo(${batEscapeEchoLine(line)}` : 'echo(');
3411
+ });
3412
+ arr.push(')');
3413
+ });
3414
+ }
3415
+
3416
+ let scriptContent = '';
3417
+ let scriptName = '';
3418
+
3419
+ // ─── WINDOWS .BAT ────────────────────────────────────────────────────────
3420
+ if (state.nativeOs === 'win') {
3421
+ const isDocker = state.deployMode === 'docker';
3422
+ scriptName = isDocker ? 'setup-openclaw-docker-win.bat' : 'setup-openclaw-win.bat';
3423
+ const lines = [
3424
+ '@echo off',
3425
+ 'chcp 65001 >nul',
3426
+ `echo === OpenClaw Setup — Windows${isDocker ? ' Docker' : ' Native'} ===`,
3427
+ 'echo.',
3428
+ 'echo [1/5] Kiem tra Node.js...',
3429
+ 'where node >nul 2>&1 || (echo ERROR: Node.js chua cai! Tai tai: https://nodejs.org && pause && exit /b 1)',
3430
+ 'echo [2/5] Cai OpenClaw CLI...',
3431
+ 'npm install -g openclaw@latest',
3432
+ ];
3433
+ providerLines(lines, 'bat');
3434
+ if (pluginCmd) { lines.push('echo Cai plugins...'); lines.push(pluginCmd); }
3435
+
3436
+ if (isMultiBot) {
3437
+ lines.push('echo [4/5] Tao runtime multi-agent dung chung...');
3438
+ appendBatWriteCommands(lines, sharedNativeFileMap());
3439
+ lines.push('echo [5/5] Khoi dong gateway multi-bot...');
3440
+ lines.push('openclaw gateway run');
3441
+ } else {
3442
+ lines.push('echo [4/5] Tao file cau hinh...');
3443
+ appendBatWriteCommands(lines, botFiles(0));
3444
+ lines.push('echo [5/5] Khoi dong bot...');
3445
+ lines.push('openclaw gateway run');
3446
+ }
3447
+
3448
+ lines.push('pause');
3449
+ scriptContent = lines.filter(Boolean).join('\r\n');
3450
+
3451
+ // ─── macOS .SH ───────────────────────────────────────────────────────────
3452
+ } else if (state.nativeOs === 'linux') {
3453
+ const isDocker = state.deployMode === 'docker';
3454
+ scriptName = isDocker ? 'setup-openclaw-docker-macos.sh' : 'setup-openclaw-macos.sh';
3455
+ const sh = [
3456
+ '#!/usr/bin/env bash', 'set -e',
3457
+ `echo "=== OpenClaw Setup — macOS${isDocker ? ' Docker' : ' Native'} ==="`,
3458
+ 'command -v node > /dev/null 2>&1 || { echo "ERROR: Node.js chua cai! https://nodejs.org"; exit 1; }',
3459
+ 'npm install -g openclaw@latest',
3460
+ ];
3461
+ providerLines(sh, 'sh');
3462
+ if (pluginCmd) sh.push(pluginCmd);
3463
+
3464
+ if (isMultiBot) {
3465
+ appendShWriteCommands(sh, sharedNativeFileMap());
3466
+ sh.push('echo "Starting shared multi-bot gateway..."');
3467
+ sh.push('openclaw gateway run');
3468
+ } else {
3469
+ appendShWriteCommands(sh, botFiles(0));
3470
+ sh.push('openclaw gateway run');
3471
+ }
3472
+ scriptContent = sh.filter(Boolean).join('\n');
3473
+
3474
+ // ─── VPS/Ubuntu PM2 .SH ──────────────────────────────────────────────────
3475
+ } else if (state.nativeOs === 'vps') {
3476
+ scriptName = 'setup-openclaw-vps.sh';
3477
+ const vps = [
3478
+ '#!/usr/bin/env bash', 'set -e',
3479
+ `echo "=== OpenClaw Setup — Ubuntu/VPS${isMultiBot ? ` Multi-Bot (${state.botCount} bots)` : ''} ==="`,
3480
+ '# Auto-install Node.js 20 LTS if missing',
3481
+ 'if ! command -v node > /dev/null 2>&1; then',
3482
+ ' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
3483
+ ' sudo apt-get install -y nodejs',
3484
+ 'fi',
3485
+ 'npm install -g openclaw@latest pm2',
3486
+ ];
3487
+ providerLines(vps, 'sh');
3488
+ if (pluginCmd) vps.push(pluginCmd);
3489
+
3490
+ if (isMultiBot) {
3491
+ vps.push('echo "--- Creating shared multi-agent runtime ---"');
3492
+ appendShWriteCommands(vps, sharedNativeFileMap());
3493
+ vps.push('echo "--- Starting shared gateway via PM2 ---"');
3494
+ vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
3495
+ vps.push('pm2 save && pm2 startup');
3496
+ vps.push(`echo ""`);
3497
+ vps.push(`echo "=== ✅ Shared multi-bot gateway running via PM2 ==="`);
3498
+ vps.push(`echo "Commands:"`);
3499
+ vps.push(`echo " pm2 status # Status gateway"`);
3500
+ vps.push(`echo " pm2 logs openclaw-multibot"`);
3501
+ } else {
3502
+ appendShWriteCommands(vps, botFiles(0));
3503
+ vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
3504
+ vps.push('pm2 save && pm2 startup');
3505
+ vps.push('echo "Bot dang chay! Xem log: pm2 logs openclaw"');
3506
+ }
3507
+ scriptContent = vps.filter(Boolean).join('\n');
3508
+
3509
+ // ─── Linux Desktop .SH ───────────────────────────────────────────────────
3510
+ } else if (state.nativeOs === 'linux-desktop') {
3511
+ scriptName = 'setup-openclaw-linux.sh';
3512
+ const lnx = [
3513
+ '#!/usr/bin/env bash', 'set -e',
3514
+ `echo "=== OpenClaw Setup — Linux Desktop${isMultiBot ? ' Multi-Bot' : ''} ==="`,
3515
+ 'if ! command -v node > /dev/null 2>&1; then',
3516
+ ' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
3517
+ ' sudo apt-get install -y nodejs',
3518
+ 'fi',
3519
+ 'npm install -g openclaw@latest',
3520
+ ];
3521
+ providerLines(lnx, 'sh');
3522
+ if (pluginCmd) lnx.push(pluginCmd);
3523
+
3524
+ if (isMultiBot) {
3525
+ appendShWriteCommands(lnx, sharedNativeFileMap());
3526
+ lnx.push('echo "Starting shared multi-bot gateway..."');
3527
+ lnx.push('openclaw gateway run');
3528
+ } else {
3529
+ appendShWriteCommands(lnx, botFiles(0));
3530
+ lnx.push('openclaw gateway run');
3531
+ }
3532
+ scriptContent = lnx.filter(Boolean).join('\n');
3533
+ }
3534
+
3535
+ // Store for download
3536
+ window._nativeScript = { name: scriptName, content: scriptContent };
3537
+
3538
+ // Update UI elements in step 5
3539
+ const nameEl = document.getElementById('native-script-name');
3540
+ if (nameEl) nameEl.textContent = scriptName;
3541
+ const instrEl = document.getElementById('native-instructions');
3542
+ if (instrEl) {
3543
+ instrEl.innerHTML = state.nativeOs === 'win'
3544
+ ? (isVi ? 'Tải file → double-click chạy ngay (tự động cài mọi thứ)' : 'Download → double-click to run (installs everything automatically)')
3545
+ : (isVi ? `Tải file → <code>chmod +x ${scriptName} && ./${scriptName}</code>` : `Download → <code>chmod +x ${scriptName} && ./${scriptName}</code>`);
3546
+ }
3547
+
3548
+ // Populate auto-steps summary
3549
+ const stepsList = document.getElementById('auto-steps-list');
3550
+ if (stepsList) {
3551
+ const steps = [];
3552
+ steps.push(isVi ? '✅ Kiểm tra Node.js (cài tự động trên Ubuntu/VPS nếu chưa có)' : '✅ Check Node.js (auto-install on Ubuntu/VPS if missing)');
3553
+ steps.push(isVi ? '📦 Cài OpenClaw CLI (<code>npm install -g openclaw@latest</code>)' : '📦 Install OpenClaw CLI (<code>npm install -g openclaw@latest</code>)');
3554
+ if (is9Router) {
3555
+ steps.push(isVi ? '🔀 Cài 9Router (<code>npm install -g 9router</code>) và khởi động tự động' : '🔀 Install 9Router (<code>npm install -g 9router</code>) and start automatically');
3556
+ } else if (isOllama) {
3557
+ steps.push(isVi ? `🦙 Cài Ollama (nếu chưa có) và pull model <code>${selectedModel}</code>` : `🦙 Install Ollama (if missing) and pull model <code>${selectedModel}</code>`);
3558
+ }
3559
+ if (pluginCmd) steps.push(isVi ? '🧩 Cài plugins đã chọn' : '🧩 Install selected plugins');
3560
+ if (isMultiBot) {
3561
+ steps.push(isVi ? '🧩 Tạo một runtime multi-agent dùng chung cho toàn bộ bot' : '🧩 Create one shared multi-agent runtime for the full bot team');
3562
+ steps.push(isVi ? '🔀 Khai báo Telegram multi-account + bindings + agent-to-agent handoff' : '🔀 Configure Telegram multi-account + bindings + agent-to-agent handoff');
3563
+ steps.push(state.nativeOs === 'vps'
3564
+ ? (isVi ? '🚀 Khởi động shared gateway qua PM2 (tự restart sau reboot)' : '🚀 Start the shared gateway via PM2 (auto-restart on reboot)')
3565
+ : (isVi ? '🚀 Khởi động một shared gateway cho toàn bộ bot' : '🚀 Start one shared gateway for all bots'));
3566
+ } else {
3567
+ const usePm2 = state.nativeOs === 'vps';
3568
+ steps.push(usePm2
3569
+ ? (isVi ? '🚀 Khởi động bot qua PM2 (tự restart sau reboot)' : '🚀 Start bot via PM2 (auto-restart on reboot)')
3570
+ : (isVi ? '🚀 Khởi động bot' : '🚀 Start bot'));
3571
+ }
3572
+ stepsList.innerHTML = steps.map((s) => `<div style="margin: 4px 0; padding-left: 4px;">${s}</div>`).join('');
3573
+ }
3574
+ }
3575
+
3576
+
3577
+
3578
+ window.downloadNativeScript = function() {
3579
+ const script = window._nativeScript;
3580
+ if (!script) return;
3581
+ const blob = new Blob([script.content], { type: 'text/plain;charset=utf-8' });
3582
+ const url = URL.createObjectURL(blob);
3583
+ const a = document.createElement('a');
3584
+ a.href = url; a.download = script.name; a.style.display = 'none';
3585
+ document.body.appendChild(a); a.click();
3586
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 1000);
3587
+ };
1815
3588
 
1816
3589
  // ========== Generate Windows Auto Setup .bat ==========
3590
+
1817
3591
  function generateAutoSetupBat() {
1818
3592
  const files = state._generatedFiles;
1819
3593
  if (!files) return '';
@@ -1928,17 +3702,37 @@ ${ps}`;
1928
3702
  return bat;
1929
3703
  }
1930
3704
 
1931
- // Download .bat file
3705
+ // Download Docker setup file — format + name based on OS
1932
3706
  function downloadAutoSetupBat() {
1933
3707
  // Regenerate output first to ensure state._generatedFiles is current
1934
3708
  generateOutput();
1935
- const content = generateAutoSetupBat();
1936
- const winContent = content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
1937
- const blob = new Blob([winContent], { type: 'application/x-bat;charset=utf-8' });
3709
+
3710
+ const os = state.nativeOs || 'win';
3711
+ const isWindows = os === 'win';
3712
+
3713
+ let filename, blob;
3714
+ if (isWindows) {
3715
+ // Windows: PowerShell wrapped in .bat
3716
+ const content = generateAutoSetupBat();
3717
+ const winContent = content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
3718
+ filename = 'setup-openclaw-docker-win.bat';
3719
+ blob = new Blob([winContent], { type: 'application/x-bat;charset=utf-8' });
3720
+ } else {
3721
+ // macOS / Linux / VPS: bash script
3722
+ const content = generateSetupScript(state._generatedFiles);
3723
+ const osLabel = os === 'linux' ? 'macos' : (os === 'vps' ? 'vps' : os);
3724
+ filename = `setup-openclaw-docker-${osLabel}.sh`;
3725
+ blob = new Blob([content], { type: 'text/x-shellscript;charset=utf-8' });
3726
+ }
3727
+
3728
+ // Update button label in UI
3729
+ const dlLabel = document.getElementById('docker-dl-filename');
3730
+ if (dlLabel) dlLabel.textContent = filename;
3731
+
1938
3732
  const url = URL.createObjectURL(blob);
1939
3733
  const a = document.createElement('a');
1940
3734
  a.href = url;
1941
- a.download = 'setup-openclaw.bat';
3735
+ a.download = filename;
1942
3736
  document.body.appendChild(a);
1943
3737
  a.click();
1944
3738
  document.body.removeChild(a);
@@ -1946,60 +3740,111 @@ ${ps}`;
1946
3740
  }
1947
3741
  window.downloadAutoSetupBat = downloadAutoSetupBat;
1948
3742
 
3743
+ // Call on step 5 render to pre-set docker DL filename
3744
+ function updateDockerDlLabel() {
3745
+ const os = state.nativeOs || 'win';
3746
+ const isWindows = os === 'win';
3747
+ const lbl = document.getElementById('docker-dl-filename');
3748
+ const icon = document.getElementById('docker-dl-icon');
3749
+ const title = document.getElementById('docker-dl-title');
3750
+ const desc = document.getElementById('docker-dl-desc');
3751
+ const winNote = document.getElementById('docker-dl-win-note');
3752
+ const shNote = document.getElementById('docker-dl-sh-note');
3753
+ const shCmd = document.getElementById('docker-dl-sh-cmd');
3754
+
3755
+ if (isWindows) {
3756
+ if (lbl) lbl.textContent = 'setup-openclaw-docker-win.bat';
3757
+ if (icon) icon.textContent = '🪟';
3758
+ if (title) { title.textContent = 'Cách 1: Windows — Download & Double-click'; }
3759
+ if (desc) { desc.textContent = 'Tải file .bat → double-click → tự động cài Docker, pull model và khởi động bot.'; }
3760
+ if (winNote) winNote.style.display = 'block';
3761
+ if (shNote) shNote.style.display = 'none';
3762
+ } else {
3763
+ const osLabel = os === 'linux' ? 'macos' : (os === 'vps' ? 'vps' : os);
3764
+ const fn = `setup-openclaw-docker-${osLabel}.sh`;
3765
+ if (lbl) lbl.textContent = fn;
3766
+ if (icon) icon.textContent = '💻';
3767
+ if (title) { title.textContent = `Cách 1: ${osLabel === 'macos' ? 'macOS/Linux' : 'VPS'} — Tải Bash Script`; }
3768
+ if (desc) { desc.textContent = `Tải file .sh về và chạy lệnh trong Terminal để cài đặt hệ thống.`; }
3769
+ if (winNote) winNote.style.display = 'none';
3770
+ if (shNote) {
3771
+ shNote.style.display = 'block';
3772
+ if (shCmd) shCmd.innerHTML = `chmod +x <span class="docker-dl-fn-copy">${fn}</span> && ./<span class="docker-dl-fn-copy">${fn}</span>`;
3773
+ }
3774
+ }
3775
+ }
3776
+
1949
3777
  // ========== Generate Setup Bash Script ==========
1950
3778
  function generateSetupScript(files) {
1951
3779
  if (!files) return '# No files generated';
1952
3780
  const projectDir = document.getElementById('cfg-project-path')?.value?.trim() || '.';
1953
3781
  const lang = document.getElementById('cfg-language')?.value || 'vi';
1954
3782
  const isVi = lang === 'vi';
3783
+ const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
1955
3784
 
1956
3785
  let script = `#!/bin/bash
1957
- # 🦞 OpenClaw Setup Script
3786
+ # 🦞 OpenClaw Setup Script${isMultiBot ? ` — Multi-Bot (${state.botCount} bots)` : ''}
1958
3787
  # ${isVi ? 'Tạo bởi OpenClaw Wizard — paste vào terminal trong thư mục project' : 'Generated by OpenClaw Wizard — paste into terminal in your project folder'}
1959
3788
  set -e
1960
- echo "🦞 OpenClaw Setup..."
3789
+ echo "🦞 OpenClaw Setup${isMultiBot ? ` (${state.botCount} bots)` : ''}..."
1961
3790
  echo ""
1962
3791
  `;
1963
3792
 
1964
- // Collect directories
3793
+ // Multi or single bot logic handles files universally
1965
3794
  const dirs = new Set();
1966
3795
  Object.keys(files).forEach(path => {
1967
3796
  const dir = path.substring(0, path.lastIndexOf('/'));
1968
3797
  if (dir) dirs.add(dir);
1969
3798
  });
1970
3799
 
1971
- // Create directories
1972
- script += `# ${isVi ? 'Tạo thư mục' : 'Create directories'}\n`;
3800
+ script += `# \${isVi ? 'Tạo thư mục' : 'Create directories'}\n`;
1973
3801
  Array.from(dirs).sort().forEach(dir => {
1974
- script += `mkdir -p "${dir}"\n`;
3802
+ script += `mkdir -p "\${dir}"\n`;
1975
3803
  });
1976
3804
  script += '\n';
1977
3805
 
1978
- // Write each file using heredoc
1979
3806
  Object.entries(files).forEach(([path, content]) => {
1980
- script += `# ${path}\n`;
1981
- script += `cat > "${path}" << 'CLAWEOF'\n`;
1982
- script += content;
1983
- if (!content.endsWith('\n')) script += '\n';
3807
+ script += `# \${path}\n`;
3808
+ const contentStr = typeof content === 'string' ? content : '';
3809
+ script += `cat > "\${path}" << 'CLAWEOF'\n`;
3810
+ script += contentStr;
3811
+ if (!contentStr.endsWith('\n')) script += '\n';
1984
3812
  script += `CLAWEOF\n\n`;
1985
3813
  });
1986
-
1987
- // Success message
1988
- script += `echo ""\n`;
1989
- script += `echo "${isVi ? '✅ Tạo xong! Các file đã được tạo:' : '✅ Done! Files created:'}"\n`;
1990
- script += `echo " .openclaw/ — ${isVi ? 'Config bot' : 'Bot config'}"\n`;
1991
- script += `echo " docker/openclaw/ — Docker files"\n`;
3814
+
1992
3815
  script += `echo ""\n`;
1993
- script += `echo "${isVi ? '📝 Bước tiếp theo:' : '📝 Next steps:'}"\n`;
1994
- script += `echo "${isVi ? ' 1. Sửa docker/openclaw/.env → paste API keys thật' : ' 1. Edit docker/openclaw/.env → paste real API keys'}"\n`;
1995
- script += `echo "${isVi ? ' 2. cd docker/openclaw && docker compose build && docker compose up -d' : ' 2. cd docker/openclaw && docker compose build && docker compose up -d'}"\n`;
3816
+ script += `echo "\${isVi ? ' Tạo file xong!' : ' Files created!'}"\n`;
1996
3817
  script += `echo ""\n`;
3818
+ script += `echo "\${isVi ? '🐳 Đang khởi động Docker (có thể mất vài phút)...' : '🐳 Starting Docker (may take a few minutes)...'}"\n`;
3819
+ script += `if docker compose version > /dev/null 2>&1; then\n COMPOSE_CMD="docker compose"\nelif docker-compose version > /dev/null 2>&1; then\n COMPOSE_CMD="docker-compose"\nelse\n echo "\${isVi ? '❌ Không tìm thấy Docker Compose! Cài bằng: sudo apt-get install docker-compose-plugin' : '❌ Docker Compose not found! Install: sudo apt-get install docker-compose-plugin'}"\n exit 1\nfi\n`;
3820
+
3821
+ if (isMultiBot) {
3822
+ script += `cd "docker/openclaw"\n`;
3823
+ script += `$COMPOSE_CMD up --detach --build\n`;
3824
+ script += `if [ $? -ne 0 ]; then\n echo "\${isVi ? '❌ Docker build thất bại.' : '❌ Docker build failed.'}"\n exit 1\nfi\n`;
3825
+ script += `echo ""\n`;
3826
+ script += `echo "${isVi ? 'OK: ${state.botCount} bot dang chay!' : 'OK: ${state.botCount} bots are running!'}"\n`;
3827
+ for (let i = 0; i < state.botCount; i++) {
3828
+ const botName = (state.bots[i]?.name || `bot${i + 1}`).replace(/\s+/g, '-').toLowerCase();
3829
+ script += `echo " - openclaw-${botName} (port ${18791 + i})"\n`;
3830
+ }
3831
+ script += `echo ""\n`;
3832
+ } else {
3833
+ script += `cd "docker/openclaw"\n`;
3834
+ script += `$COMPOSE_CMD up --detach --build\n`;
3835
+ script += `if [ $? -ne 0 ]; then\n echo "\${isVi ? '❌ Docker build thất bại.' : '❌ Docker build failed.'}"\n exit 1\nfi\n`;
3836
+ script += `echo "\${isVi ? '🎉 Bot đang chạy! Xem log qua:' : '🎉 Bot is running! View logs:'}"\n`;
3837
+ script += `echo " docker logs -f openclaw-bot"\n`;
3838
+ script += `echo ""\n`;
3839
+ }
1997
3840
  script += `echo "🦞 Happy botting!"\n`;
1998
3841
 
1999
3842
  return script;
2000
3843
  }
2001
3844
 
3845
+
2002
3846
  // ========== Zalo Personal Onboard Guide (post-Docker-setup) ==========
3847
+
2003
3848
  function generateZaloOnboardGuide() {
2004
3849
  const lang = document.getElementById('cfg-language')?.value || 'vi';
2005
3850
  setOutput('out-zalo-onboard-cmd', `docker exec -it openclaw-bot openclaw onboard`);
@@ -2127,3 +3972,4 @@ echo ""
2127
3972
  setTimeout(() => { btnEl.innerHTML = originalText; }, 2500);
2128
3973
  };
2129
3974
  })();
3975
+