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