create-openclaw-bot 5.0.0 → 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/CHANGELOG.md +53 -0
- package/CHANGELOG.vi.md +53 -0
- package/README.md +321 -237
- package/README.vi.md +321 -240
- package/cli.js +1614 -852
- package/docs/ai-providers.md +144 -0
- package/docs/ai-providers.vi.md +144 -0
- package/docs/faq.md +63 -0
- package/docs/faq.vi.md +63 -0
- package/docs/hardware-guide.md +55 -0
- package/docs/hardware-guide.vi.md +55 -0
- package/docs/install-docker.md +160 -0
- package/docs/install-docker.vi.md +160 -0
- package/docs/install-native.md +96 -0
- package/docs/install-native.vi.md +96 -0
- package/docs/preview.png +0 -0
- package/index.html +577 -344
- package/package.json +1 -1
- package/setup.js +2013 -187
- package/style.css +105 -0
- /package/{SETUP.md → docs/SETUP.md} +0 -0
- /package/{SETUP.vi.md → docs/SETUP.vi.md} +0 -0
package/setup.js
CHANGED
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
// ========== State ==========
|
|
27
27
|
const state = {
|
|
28
28
|
currentStep: 1,
|
|
29
|
-
totalSteps:
|
|
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: {
|
|
@@ -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
|
|
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
|
|
474
|
+
// ========== Step 2: Channel Selection ==========
|
|
470
475
|
function bindChannelCards() {
|
|
471
|
-
document.
|
|
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
|
-
|
|
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 & 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 & 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
|
-
|
|
488
|
-
if (state.currentStep === 2)
|
|
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
|
-
|
|
506
|
-
if (step === 3)
|
|
507
|
-
if (step === 4)
|
|
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
|
-
|
|
545
|
-
|
|
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
|
-
|
|
914
|
+
const userInfoVal = document.getElementById('cfg-user-info')?.value?.trim();
|
|
915
|
+
if (!nameVal || !userInfoVal) isDisabled = true;
|
|
548
916
|
}
|
|
549
|
-
|
|
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 ===
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
708
|
-
|
|
1091
|
+
|
|
1092
|
+
// Security rules
|
|
709
1093
|
const securityEl = document.getElementById('cfg-security');
|
|
710
|
-
if (securityEl
|
|
711
|
-
|
|
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
|
-
|
|
718
|
-
|
|
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];
|
|
@@ -778,10 +1225,10 @@
|
|
|
778
1225
|
? '🐳 Ollama sẽ tự chạy trong Docker cùng bot. Model được tải tự động khi <code>docker compose up</code>.'
|
|
779
1226
|
: '🐳 Ollama runs automatically as a Docker sidecar. Model is pulled automatically on first <code>docker compose up</code>.'}
|
|
780
1227
|
</p>`;
|
|
781
|
-
pHtml += `<p style="font-size: 12px; color: var(--text-muted); margin: 0;">
|
|
1228
|
+
pHtml += `<p style="font-size: 12px; color: var(--text-muted); margin: 4px 0 0;">
|
|
782
1229
|
${isVi
|
|
783
|
-
? '
|
|
784
|
-
: '
|
|
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.'}
|
|
785
1232
|
</p>`;
|
|
786
1233
|
} else {
|
|
787
1234
|
// Direct API provider: show key input
|
|
@@ -807,11 +1254,28 @@
|
|
|
807
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>`;
|
|
808
1255
|
|
|
809
1256
|
if (state.channel === 'telegram') {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
+
}
|
|
815
1279
|
} else if (state.channel === 'zalo-bot') {
|
|
816
1280
|
cHtml += `<div class="form-group" style="margin: 0;">
|
|
817
1281
|
<label class="form-group__label" for="key-bot-token">🔑 Zalo Bot Token <span style="color: var(--danger, #ef4444);">*</span></label>
|
|
@@ -831,6 +1295,7 @@
|
|
|
831
1295
|
channelEl.innerHTML = cHtml;
|
|
832
1296
|
}
|
|
833
1297
|
|
|
1298
|
+
|
|
834
1299
|
// ─── Section 3: Skill env vars ───
|
|
835
1300
|
const skillsEl = document.getElementById('key-section-skills');
|
|
836
1301
|
if (skillsEl) {
|
|
@@ -853,6 +1318,22 @@
|
|
|
853
1318
|
});
|
|
854
1319
|
skillsEl.innerHTML = sHtml;
|
|
855
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
|
+
}
|
|
856
1337
|
}
|
|
857
1338
|
window.__validateKeys = function() { updateNavButtons(); };
|
|
858
1339
|
// 9Router API keys are managed via its dashboard — no client-side generation needed
|
|
@@ -868,16 +1349,32 @@
|
|
|
868
1349
|
|
|
869
1350
|
const lines = [];
|
|
870
1351
|
const apiKeyVal = document.getElementById('key-api-key')?.value?.trim() || '';
|
|
871
|
-
const botTokenVal = document.getElementById('key-bot-token')?.value?.trim()
|
|
1352
|
+
const botTokenVal = document.getElementById('key-bot-token')?.value?.trim()
|
|
1353
|
+
|| state.config.botToken || '';
|
|
872
1354
|
|
|
873
1355
|
if (provider.isProxy) {
|
|
874
1356
|
lines.push('# Không cần AI API key — 9Router xử lý qua dashboard');
|
|
875
1357
|
} else if (provider.isLocal) {
|
|
876
1358
|
lines.push('OLLAMA_HOST=http://ollama:11434');
|
|
1359
|
+
lines.push('OLLAMA_API_KEY=ollama-local');
|
|
877
1360
|
} else {
|
|
878
1361
|
lines.push(`${provider.envKey}=${apiKeyVal || '<your_' + provider.envKey.toLowerCase() + '>'}`);
|
|
879
1362
|
}
|
|
880
|
-
|
|
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) {
|
|
881
1378
|
if (botTokenVal) {
|
|
882
1379
|
lines.push(ch.envExtra.replace(/=<[^>]+>$/, '=' + botTokenVal));
|
|
883
1380
|
} else {
|
|
@@ -909,6 +1406,7 @@
|
|
|
909
1406
|
envContent.textContent = lines.join('\n');
|
|
910
1407
|
}
|
|
911
1408
|
|
|
1409
|
+
|
|
912
1410
|
// ========== Step 4: Generate Output ==========
|
|
913
1411
|
function generateOutput() {
|
|
914
1412
|
const ch = CHANNELS[state.channel];
|
|
@@ -923,6 +1421,28 @@
|
|
|
923
1421
|
if (!provider) return;
|
|
924
1422
|
|
|
925
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
|
+
|
|
926
1446
|
|
|
927
1447
|
// Show/hide 9Router post-setup notice
|
|
928
1448
|
const routerNotice = document.getElementById('9router-notice');
|
|
@@ -990,9 +1510,15 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
990
1510
|
setOutput('out-task-ps1', taskPs1);
|
|
991
1511
|
}
|
|
992
1512
|
|
|
993
|
-
// Show
|
|
1513
|
+
// Show/hide docker vs native output based on deployMode
|
|
994
1514
|
const dockerOut = document.getElementById('docker-output');
|
|
995
|
-
|
|
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();
|
|
996
1522
|
|
|
997
1523
|
// Show/hide Zalo Personal onboard notice
|
|
998
1524
|
const zaloNotice = document.getElementById('zalo-onboard-notice');
|
|
@@ -1002,15 +1528,36 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1002
1528
|
if (isZaloPersonal) generateZaloOnboardGuide();
|
|
1003
1529
|
}
|
|
1004
1530
|
|
|
1005
|
-
//
|
|
1531
|
+
// Update step 5 heading
|
|
1532
|
+
const lang5 = document.getElementById('cfg-language')?.value || 'vi';
|
|
1006
1533
|
const title = document.getElementById('step4-title');
|
|
1007
1534
|
const desc = document.getElementById('step4-desc');
|
|
1008
|
-
if (title) title.textContent =
|
|
1009
|
-
if (desc) desc.textContent =
|
|
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.';
|
|
1010
1539
|
|
|
1011
1540
|
const agentId = state.config.botName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, '') || 'chat';
|
|
1012
1541
|
|
|
1013
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
|
+
: [];
|
|
1014
1561
|
|
|
1015
1562
|
// 1. openclaw.json
|
|
1016
1563
|
const clawConfig = {
|
|
@@ -1019,6 +1566,8 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1019
1566
|
defaults: {
|
|
1020
1567
|
model: { primary: state.config.model, fallbacks: [] },
|
|
1021
1568
|
compaction: { mode: 'safeguard' },
|
|
1569
|
+
timeoutSeconds: isLocal ? 900 : 120,
|
|
1570
|
+
...(isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
|
|
1022
1571
|
},
|
|
1023
1572
|
list: [{
|
|
1024
1573
|
id: agentId,
|
|
@@ -1037,13 +1586,16 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1037
1586
|
};
|
|
1038
1587
|
|
|
1039
1588
|
// 9Router: add proxy endpoint config under models.providers
|
|
1040
|
-
//
|
|
1589
|
+
// Native mode: 9router runs on localhost; Docker mode: uses docker service hostname
|
|
1041
1590
|
if (is9Router) {
|
|
1591
|
+
const nineRouterBase = state.deployMode === 'native'
|
|
1592
|
+
? 'http://localhost:20128/v1'
|
|
1593
|
+
: 'http://9router:20128/v1';
|
|
1042
1594
|
clawConfig.models = {
|
|
1043
1595
|
mode: 'merge',
|
|
1044
1596
|
providers: {
|
|
1045
1597
|
'9router': {
|
|
1046
|
-
baseUrl:
|
|
1598
|
+
baseUrl: nineRouterBase,
|
|
1047
1599
|
apiKey: 'sk-no-key',
|
|
1048
1600
|
api: 'openai-completions',
|
|
1049
1601
|
models: [
|
|
@@ -1054,6 +1606,30 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1054
1606
|
};
|
|
1055
1607
|
}
|
|
1056
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
|
+
|
|
1057
1633
|
// Browser Automation: inject browser config
|
|
1058
1634
|
if (hasBrowser) {
|
|
1059
1635
|
clawConfig.browser = {
|
|
@@ -1096,9 +1672,69 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1096
1672
|
clawConfig.skills = { entries: skillEntries };
|
|
1097
1673
|
}
|
|
1098
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
|
+
|
|
1099
1728
|
setOutput('out-openclaw-json', JSON.stringify(clawConfig, null, 2));
|
|
1100
1729
|
|
|
1730
|
+
|
|
1101
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
|
+
};
|
|
1102
1738
|
const execApprovalsConfig = {
|
|
1103
1739
|
version: 1,
|
|
1104
1740
|
defaults: {
|
|
@@ -1106,10 +1742,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1106
1742
|
ask: 'off',
|
|
1107
1743
|
askFallback: 'full'
|
|
1108
1744
|
},
|
|
1109
|
-
agents:
|
|
1110
|
-
main: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true },
|
|
1111
|
-
[agentId]: { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true }
|
|
1112
|
-
}
|
|
1745
|
+
agents: execApprovalsAgents
|
|
1113
1746
|
};
|
|
1114
1747
|
setOutput('out-exec-approvals-json', JSON.stringify(execApprovalsConfig, null, 2));
|
|
1115
1748
|
|
|
@@ -1159,9 +1792,12 @@ model:
|
|
|
1159
1792
|
: '';
|
|
1160
1793
|
|
|
1161
1794
|
// Plugins install at runtime (avoids ClawHub rate limit during build)
|
|
1162
|
-
const
|
|
1163
|
-
?
|
|
1795
|
+
const relayPluginInstallCmd = isTelegramMultiBot
|
|
1796
|
+
? `${buildRelayPluginInstallCommand('openclaw')} && `
|
|
1164
1797
|
: '';
|
|
1798
|
+
const pluginInstallCmd = allPlugins.length > 0
|
|
1799
|
+
? `openclaw plugins install ${allPlugins.join(' ')} 2>/dev/null || true && ${relayPluginInstallCmd}`
|
|
1800
|
+
: relayPluginInstallCmd;
|
|
1165
1801
|
const gatewayCmd = 'openclaw gateway run';
|
|
1166
1802
|
const browserPrefix = hasBrowser
|
|
1167
1803
|
? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
|
|
@@ -1179,6 +1815,7 @@ RUN apt-get update && apt-get install -y git curl${browserAptExtra} && rm -rf /v
|
|
|
1179
1815
|
|
|
1180
1816
|
ARG CACHEBUST=${Date.now()}
|
|
1181
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);}"
|
|
1182
1819
|
WORKDIR /root/.openclaw
|
|
1183
1820
|
|
|
1184
1821
|
EXPOSE 18791
|
|
@@ -1187,21 +1824,26 @@ ${finalCmd}`;
|
|
|
1187
1824
|
|
|
1188
1825
|
setOutput('out-dockerfile', dockerfile);
|
|
1189
1826
|
|
|
1827
|
+
const isMultiBotWizard = state.botCount > 1 && state.channel === 'telegram';
|
|
1828
|
+
|
|
1190
1829
|
// 4. docker-compose.yml
|
|
1191
1830
|
// extra_hosts always needed for browser (socat → host Chrome)
|
|
1192
1831
|
const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
|
|
1193
1832
|
|
|
1194
1833
|
// ─── Dynamic Smart Route Sync Script ────────────────────────────────────────
|
|
1195
1834
|
// Background loop inside 9Router container every 30s.
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
const
|
|
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']};
|
|
1199
1839
|
console.log('[sync-combo] 9Router sync loop started...');
|
|
1200
1840
|
const sync = async () => {
|
|
1201
1841
|
try {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const a = (
|
|
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);
|
|
1205
1847
|
if (!a.length) return;
|
|
1206
1848
|
|
|
1207
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'];
|
|
@@ -1209,8 +1851,6 @@ const sync = async () => {
|
|
|
1209
1851
|
|
|
1210
1852
|
const m = a.flatMap(p => PM[p] || []);
|
|
1211
1853
|
if (!m.length) return;
|
|
1212
|
-
let db = {};
|
|
1213
|
-
try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
|
|
1214
1854
|
if (!db.combos) db.combos = [];
|
|
1215
1855
|
|
|
1216
1856
|
const c = { id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m };
|
|
@@ -1232,26 +1872,31 @@ sync();
|
|
|
1232
1872
|
setInterval(sync, INTERVAL);`;
|
|
1233
1873
|
|
|
1234
1874
|
let compose;
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
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
|
|
1237
1885
|
services:
|
|
1238
1886
|
ai-bot:
|
|
1239
1887
|
build: .
|
|
1240
|
-
container_name: openclaw-
|
|
1888
|
+
container_name: openclaw-multibot
|
|
1241
1889
|
restart: always
|
|
1242
1890
|
env_file:
|
|
1243
1891
|
- .env
|
|
1244
|
-
|
|
1245
|
-
- 9router
|
|
1246
|
-
${extraHostsBlock}
|
|
1247
|
-
volumes:
|
|
1892
|
+
${dependsOn}${extraHosts} volumes:
|
|
1248
1893
|
- ../../.openclaw:/root/.openclaw
|
|
1249
1894
|
ports:
|
|
1250
1895
|
- "18791:18791"
|
|
1251
1896
|
|
|
1252
1897
|
9router:
|
|
1253
1898
|
image: node:22-slim
|
|
1254
|
-
container_name: 9router
|
|
1899
|
+
container_name: 9router-multibot
|
|
1255
1900
|
restart: always
|
|
1256
1901
|
entrypoint:
|
|
1257
1902
|
- /bin/sh
|
|
@@ -1274,7 +1919,63 @@ ${extraHostsBlock}
|
|
|
1274
1919
|
|
|
1275
1920
|
volumes:
|
|
1276
1921
|
9router-data:`;
|
|
1277
|
-
|
|
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) {
|
|
1278
1979
|
compose = `name: oc-bot
|
|
1279
1980
|
services:
|
|
1280
1981
|
ai-bot:
|
|
@@ -1283,18 +1984,107 @@ services:
|
|
|
1283
1984
|
restart: always
|
|
1284
1985
|
env_file:
|
|
1285
1986
|
- .env
|
|
1987
|
+
depends_on:
|
|
1988
|
+
- 9router
|
|
1286
1989
|
${extraHostsBlock}
|
|
1287
1990
|
volumes:
|
|
1288
1991
|
- ../../.openclaw:/root/.openclaw
|
|
1289
1992
|
ports:
|
|
1290
|
-
- "18791:18791"
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
setOutput('out-compose', compose);
|
|
1993
|
+
- "18791:18791"
|
|
1294
1994
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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);
|
|
2084
|
+
|
|
2085
|
+
// 5. Docker commands
|
|
2086
|
+
const approveNote = (document.getElementById('cfg-language')?.value || 'vi') === 'vi'
|
|
2087
|
+
? `\n# ⚠️ Nếu bot không tạo được cron job (lỗi pairing):\n# docker exec -i openclaw-bot openclaw devices approve --latest`
|
|
1298
2088
|
: `\n# ⚠️ If bot can't create cron jobs (pairing error):\n# docker exec -i openclaw-bot openclaw devices approve --latest`;
|
|
1299
2089
|
if (is9Router) {
|
|
1300
2090
|
setOutput('out-commands', `cd docker/openclaw
|
|
@@ -1315,35 +2105,55 @@ docker logs -f openclaw-bot${approveNote}`);
|
|
|
1315
2105
|
|
|
1316
2106
|
|
|
1317
2107
|
// 6. Generate auth-profiles.json (root + agent level)
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
type: 'api_key',
|
|
1331
|
-
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
|
+
},
|
|
1332
2120
|
},
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
+
}
|
|
1338
2144
|
const authProfilesStr = JSON.stringify(authProfilesJson, null, 2);
|
|
1339
2145
|
|
|
1340
2146
|
// 7. Generate ALL workspace Markdown files
|
|
1341
2147
|
// OpenClaw auto-injects these into agent context at the start of every session.
|
|
1342
2148
|
// Hierarchy: per-agent files → global workspace files → config defaults.
|
|
1343
|
-
const botName =
|
|
2149
|
+
const botName = isMultiBotWizard
|
|
2150
|
+
? (state.bots[0]?.name || state.config.botName || 'Chat Bot')
|
|
2151
|
+
: (state.config.botName || 'Chat Bot');
|
|
1344
2152
|
const lang = state.config.language || 'vi';
|
|
1345
2153
|
const userPrompt = state.config.systemPrompt || '';
|
|
1346
|
-
const descText =
|
|
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'));
|
|
1347
2157
|
|
|
1348
2158
|
const botEmoji = state.config.emoji || '🤖';
|
|
1349
2159
|
|
|
@@ -1784,29 +2594,158 @@ fi
|
|
|
1784
2594
|
`;
|
|
1785
2595
|
|
|
1786
2596
|
// Store generated files for download
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
+
}
|
|
1810
2749
|
|
|
1811
2750
|
// Generate setup bash script
|
|
1812
2751
|
const setupScript = generateSetupScript(state._generatedFiles);
|
|
@@ -1816,11 +2755,839 @@ fi
|
|
|
1816
2755
|
const envFinal = document.getElementById('out-env-final');
|
|
1817
2756
|
const envContent = document.getElementById('env-content');
|
|
1818
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
|
+
}
|
|
2814
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
1819
3574
|
}
|
|
1820
3575
|
|
|
1821
3576
|
|
|
1822
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
|
+
};
|
|
3588
|
+
|
|
1823
3589
|
// ========== Generate Windows Auto Setup .bat ==========
|
|
3590
|
+
|
|
1824
3591
|
function generateAutoSetupBat() {
|
|
1825
3592
|
const files = state._generatedFiles;
|
|
1826
3593
|
if (!files) return '';
|
|
@@ -1935,17 +3702,37 @@ ${ps}`;
|
|
|
1935
3702
|
return bat;
|
|
1936
3703
|
}
|
|
1937
3704
|
|
|
1938
|
-
// Download
|
|
3705
|
+
// Download Docker setup file — format + name based on OS
|
|
1939
3706
|
function downloadAutoSetupBat() {
|
|
1940
3707
|
// Regenerate output first to ensure state._generatedFiles is current
|
|
1941
3708
|
generateOutput();
|
|
1942
|
-
|
|
1943
|
-
const
|
|
1944
|
-
const
|
|
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
|
+
|
|
1945
3732
|
const url = URL.createObjectURL(blob);
|
|
1946
3733
|
const a = document.createElement('a');
|
|
1947
3734
|
a.href = url;
|
|
1948
|
-
a.download =
|
|
3735
|
+
a.download = filename;
|
|
1949
3736
|
document.body.appendChild(a);
|
|
1950
3737
|
a.click();
|
|
1951
3738
|
document.body.removeChild(a);
|
|
@@ -1953,73 +3740,111 @@ ${ps}`;
|
|
|
1953
3740
|
}
|
|
1954
3741
|
window.downloadAutoSetupBat = downloadAutoSetupBat;
|
|
1955
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
|
+
|
|
1956
3777
|
// ========== Generate Setup Bash Script ==========
|
|
1957
3778
|
function generateSetupScript(files) {
|
|
1958
3779
|
if (!files) return '# No files generated';
|
|
1959
3780
|
const projectDir = document.getElementById('cfg-project-path')?.value?.trim() || '.';
|
|
1960
3781
|
const lang = document.getElementById('cfg-language')?.value || 'vi';
|
|
1961
3782
|
const isVi = lang === 'vi';
|
|
3783
|
+
const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
|
|
1962
3784
|
|
|
1963
3785
|
let script = `#!/bin/bash
|
|
1964
|
-
# 🦞 OpenClaw Setup Script
|
|
3786
|
+
# 🦞 OpenClaw Setup Script${isMultiBot ? ` — Multi-Bot (${state.botCount} bots)` : ''}
|
|
1965
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'}
|
|
1966
3788
|
set -e
|
|
1967
|
-
echo "🦞 OpenClaw Setup..."
|
|
3789
|
+
echo "🦞 OpenClaw Setup${isMultiBot ? ` (${state.botCount} bots)` : ''}..."
|
|
1968
3790
|
echo ""
|
|
1969
3791
|
`;
|
|
1970
3792
|
|
|
1971
|
-
//
|
|
3793
|
+
// Multi or single bot logic handles files universally
|
|
1972
3794
|
const dirs = new Set();
|
|
1973
3795
|
Object.keys(files).forEach(path => {
|
|
1974
3796
|
const dir = path.substring(0, path.lastIndexOf('/'));
|
|
1975
3797
|
if (dir) dirs.add(dir);
|
|
1976
3798
|
});
|
|
1977
3799
|
|
|
1978
|
-
|
|
1979
|
-
script += `# ${isVi ? 'Tạo thư mục' : 'Create directories'}\n`;
|
|
3800
|
+
script += `# \${isVi ? 'Tạo thư mục' : 'Create directories'}\n`;
|
|
1980
3801
|
Array.from(dirs).sort().forEach(dir => {
|
|
1981
|
-
script += `mkdir -p "
|
|
3802
|
+
script += `mkdir -p "\${dir}"\n`;
|
|
1982
3803
|
});
|
|
1983
3804
|
script += '\n';
|
|
1984
3805
|
|
|
1985
|
-
// Write each file using heredoc
|
|
1986
3806
|
Object.entries(files).forEach(([path, content]) => {
|
|
1987
|
-
script += `#
|
|
1988
|
-
|
|
1989
|
-
script +=
|
|
1990
|
-
|
|
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';
|
|
1991
3812
|
script += `CLAWEOF\n\n`;
|
|
1992
3813
|
});
|
|
1993
|
-
|
|
1994
|
-
// Files created — confirm then auto-run docker
|
|
3814
|
+
|
|
1995
3815
|
script += `echo ""\n`;
|
|
1996
|
-
script += `echo "
|
|
3816
|
+
script += `echo "\${isVi ? '✅ Tạo file xong!' : '✅ Files created!'}"\n`;
|
|
1997
3817
|
script += `echo ""\n`;
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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
|
+
}
|
|
2017
3840
|
script += `echo "🦞 Happy botting!"\n`;
|
|
2018
3841
|
|
|
2019
3842
|
return script;
|
|
2020
3843
|
}
|
|
2021
3844
|
|
|
3845
|
+
|
|
2022
3846
|
// ========== Zalo Personal Onboard Guide (post-Docker-setup) ==========
|
|
3847
|
+
|
|
2023
3848
|
function generateZaloOnboardGuide() {
|
|
2024
3849
|
const lang = document.getElementById('cfg-language')?.value || 'vi';
|
|
2025
3850
|
setOutput('out-zalo-onboard-cmd', `docker exec -it openclaw-bot openclaw onboard`);
|
|
@@ -2147,3 +3972,4 @@ echo ""
|
|
|
2147
3972
|
setTimeout(() => { btnEl.innerHTML = originalText; }, 2500);
|
|
2148
3973
|
};
|
|
2149
3974
|
})();
|
|
3975
|
+
|