create-openclaw-bot 5.1.13 → 5.1.15
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 +18 -1
- package/CHANGELOG.vi.md +18 -1
- package/README.md +11 -14
- package/README.vi.md +11 -14
- package/cli.js +290 -224
- package/docs/install-docker.md +3 -2
- package/docs/install-docker.vi.md +3 -2
- package/package.json +1 -1
- package/setup.js +405 -213
- package/tests/smoke-cli-logic.mjs +130 -23
package/setup.js
CHANGED
|
@@ -161,14 +161,14 @@
|
|
|
161
161
|
|
|
162
162
|
// ========== Available Plugins (npm packages — runtime/channel extensions) ==========
|
|
163
163
|
const PLUGINS = [
|
|
164
|
-
{
|
|
165
|
-
id: 'telegram-multibot-relay',
|
|
166
|
-
name: 'Telegram Multi-Bot Relay',
|
|
167
|
-
icon: '🤝',
|
|
168
|
-
descVi: 'Điều phối nhiều bot Telegram trong cùng group — tự động khi chọn nhiều bot', descEn: 'Coordinate multiple Telegram bots in one group — auto-selected with multi-bot',
|
|
169
|
-
package: 'telegram-multibot-relay',
|
|
170
|
-
hidden: true, // hidden in UI, auto-selected programmatically
|
|
171
|
-
},
|
|
164
|
+
{
|
|
165
|
+
id: 'telegram-multibot-relay',
|
|
166
|
+
name: 'Telegram Multi-Bot Relay',
|
|
167
|
+
icon: '🤝',
|
|
168
|
+
descVi: 'Điều phối nhiều bot Telegram trong cùng group — tự động khi chọn nhiều bot', descEn: 'Coordinate multiple Telegram bots in one group — auto-selected with multi-bot',
|
|
169
|
+
package: 'openclaw-telegram-multibot-relay',
|
|
170
|
+
hidden: true, // hidden in UI, auto-selected programmatically
|
|
171
|
+
},
|
|
172
172
|
{
|
|
173
173
|
id: 'voice-call',
|
|
174
174
|
name: 'Voice Call',
|
|
@@ -804,8 +804,8 @@
|
|
|
804
804
|
icon: '🐧',
|
|
805
805
|
titleVi: 'Ubuntu / VPS — Khuyên dùng Native (Không Docker)',
|
|
806
806
|
titleEn: 'Ubuntu / VPS — Recommended: Native (No Docker)',
|
|
807
|
-
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, OpenClaw CLI, PM2, 9Router/Ollama và giữ bot chạy liên tục sau reboot.',
|
|
808
|
-
descEn: 'Run directly on machine — lower RAM, faster startup. Script auto-installs Node.js 20 LTS, OpenClaw CLI, PM2, 9Router/Ollama and keeps bot running across reboots.',
|
|
807
|
+
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, OpenClaw CLI, PM2, 9Router/Ollama và giữ bot chạy liên tục sau reboot. Tạm tránh Node 25.',
|
|
808
|
+
descEn: 'Run directly on machine — lower RAM, faster startup. Script auto-installs Node.js 20 LTS, OpenClaw CLI, PM2, 9Router/Ollama and keeps bot running across reboots. Avoid Node 25 for now.',
|
|
809
809
|
deploy: 'native',
|
|
810
810
|
badgeVi: '💻 Native + PM2',
|
|
811
811
|
badgeEn: '💻 Native + PM2',
|
|
@@ -815,8 +815,8 @@
|
|
|
815
815
|
icon: '🖥️',
|
|
816
816
|
titleVi: 'Linux Desktop — Khuyên dùng Native',
|
|
817
817
|
titleEn: 'Linux Desktop — Recommended: Native',
|
|
818
|
-
descVi: 'Không cần Docker. Script tự cài Node.js 20 LTS nếu chưa có, cài OpenClaw CLI, rồi cài 9Router hoặc Ollama theo provider bạn chọn và khởi động bot ngay.',
|
|
819
|
-
descEn: 'No Docker needed. Script auto-installs Node.js 20 LTS if missing, installs OpenClaw CLI, then installs 9Router or Ollama based on your provider choice and starts the bot immediately.',
|
|
818
|
+
descVi: 'Không cần Docker. Script tự cài Node.js 20 LTS nếu chưa có, cài OpenClaw CLI, rồi cài 9Router hoặc Ollama theo provider bạn chọn và khởi động bot ngay. Tạm tránh Node 25.',
|
|
819
|
+
descEn: 'No Docker needed. Script auto-installs Node.js 20 LTS if missing, installs OpenClaw CLI, then installs 9Router or Ollama based on your provider choice and starts the bot immediately. Avoid Node 25 for now.',
|
|
820
820
|
deploy: 'native',
|
|
821
821
|
badgeVi: '💻 Native',
|
|
822
822
|
badgeEn: '💻 Native',
|
|
@@ -1049,14 +1049,18 @@
|
|
|
1049
1049
|
}).join('');
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
|
-
window.__selectProvider = function (key) {
|
|
1053
|
-
state.config.provider = key;
|
|
1054
|
-
const p = PROVIDERS[key];
|
|
1055
|
-
state.config.model = p.models[0].id;
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1052
|
+
window.__selectProvider = function (key) {
|
|
1053
|
+
state.config.provider = key;
|
|
1054
|
+
const p = PROVIDERS[key];
|
|
1055
|
+
state.config.model = p.models[0].id;
|
|
1056
|
+
if (state.bots[state.activeBotIndex]) {
|
|
1057
|
+
state.bots[state.activeBotIndex].provider = key;
|
|
1058
|
+
state.bots[state.activeBotIndex].model = p.models[0].id;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Highlight card
|
|
1062
|
+
document.querySelectorAll('.provider-card').forEach((c) => c.classList.remove('provider-card--selected'));
|
|
1063
|
+
document.querySelector(`.provider-card[data-provider="${key}"]`)?.classList.add('provider-card--selected');
|
|
1060
1064
|
|
|
1061
1065
|
// Update model dropdown
|
|
1062
1066
|
const modelSelect = document.getElementById('cfg-model');
|
|
@@ -1246,36 +1250,44 @@
|
|
|
1246
1250
|
renderBotTabBar();
|
|
1247
1251
|
}
|
|
1248
1252
|
|
|
1249
|
-
function saveFormData() {
|
|
1250
|
-
state.config.botName = document.getElementById('cfg-name')?.value || state.config.botName || 'Chat Bot';
|
|
1251
|
-
state.config.description = document.getElementById('cfg-desc')?.value || state.config.description || 'Personal AI assistant';
|
|
1252
|
-
state.config.emoji = document.getElementById('cfg-emoji')?.value || state.config.emoji || '🤖';
|
|
1253
|
-
state.config.model = document.getElementById('cfg-model')?.value || state.config.model || 'google/gemini-2.5-flash';
|
|
1253
|
+
function saveFormData() {
|
|
1254
|
+
state.config.botName = document.getElementById('cfg-name')?.value || state.config.botName || 'Chat Bot';
|
|
1255
|
+
state.config.description = document.getElementById('cfg-desc')?.value || state.config.description || 'Personal AI assistant';
|
|
1256
|
+
state.config.emoji = document.getElementById('cfg-emoji')?.value || state.config.emoji || '🤖';
|
|
1257
|
+
state.config.model = document.getElementById('cfg-model')?.value || state.config.model || 'google/gemini-2.5-flash';
|
|
1254
1258
|
state.config.language = document.getElementById('cfg-language')?.value || state.config.language || 'vi';
|
|
1255
1259
|
state.config.systemPrompt = document.getElementById('cfg-prompt')?.value || state.config.systemPrompt || DEFAULT_PROMPTS['vi'];
|
|
1256
1260
|
state.config.userInfo = document.getElementById('cfg-user-info')?.value?.trim() || state.config.userInfo || '';
|
|
1257
1261
|
state.config.securityRules = document.getElementById('cfg-security')?.value || state.config.securityRules || DEFAULT_SECURITY_RULES['vi'];
|
|
1258
1262
|
// Also save bot-tab-name → bots[0].name so both state locations stay in sync
|
|
1259
|
-
const tabName = document.getElementById('cfg-bot-tab-name')?.value?.trim();
|
|
1260
|
-
if (tabName && state.bots[0]) state.bots[0].name = tabName;
|
|
1261
|
-
else if (state.config.botName && state.bots[0] && !state.bots[0].name) {
|
|
1262
|
-
state.bots[0].name = state.config.botName;
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1263
|
+
const tabName = document.getElementById('cfg-bot-tab-name')?.value?.trim();
|
|
1264
|
+
if (tabName && state.bots[0]) state.bots[0].name = tabName;
|
|
1265
|
+
else if (state.config.botName && state.bots[0] && !state.bots[0].name) {
|
|
1266
|
+
state.bots[0].name = state.config.botName;
|
|
1267
|
+
}
|
|
1268
|
+
if (state.bots[state.activeBotIndex]) {
|
|
1269
|
+
state.bots[state.activeBotIndex].provider = state.config.provider;
|
|
1270
|
+
state.bots[state.activeBotIndex].model = state.config.model;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1265
1273
|
|
|
1266
1274
|
// Save Step 4 credential inputs to state (persists across Back navigation)
|
|
1267
|
-
function saveCredentials() {
|
|
1268
|
-
const botTokenEl = document.getElementById('key-bot-token');
|
|
1269
|
-
const apiKeyEl = document.getElementById('key-api-key');
|
|
1270
|
-
const pathEl = document.getElementById('cfg-project-path');
|
|
1271
|
-
if (botTokenEl) state.config.botToken = botTokenEl.value;
|
|
1272
|
-
if (apiKeyEl) state.config.apiKey = apiKeyEl.value;
|
|
1273
|
-
if (pathEl) state.config.projectPath = pathEl.value;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1275
|
+
function saveCredentials() {
|
|
1276
|
+
const botTokenEl = document.getElementById('key-bot-token');
|
|
1277
|
+
const apiKeyEl = document.getElementById('key-api-key');
|
|
1278
|
+
const pathEl = document.getElementById('cfg-project-path');
|
|
1279
|
+
if (botTokenEl) state.config.botToken = botTokenEl.value;
|
|
1280
|
+
if (apiKeyEl) state.config.apiKey = apiKeyEl.value;
|
|
1281
|
+
if (pathEl) state.config.projectPath = pathEl.value;
|
|
1282
|
+
if (state.botCount <= 1 && state.bots[state.activeBotIndex]) {
|
|
1283
|
+
if (botTokenEl) state.bots[state.activeBotIndex].token = botTokenEl.value;
|
|
1284
|
+
if (apiKeyEl) state.bots[state.activeBotIndex].apiKey = apiKeyEl.value;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Also save multi-bot tokens individually
|
|
1288
|
+
if (state.botCount > 1) {
|
|
1289
|
+
for (let i = 0; i < state.botCount; i++) {
|
|
1290
|
+
const el = document.getElementById(`key-bot-token-${i}`);
|
|
1279
1291
|
if (el && state.bots[i]) state.bots[i].token = el.value;
|
|
1280
1292
|
}
|
|
1281
1293
|
}
|
|
@@ -1534,7 +1546,7 @@
|
|
|
1534
1546
|
const is9Router = provider.isProxy;
|
|
1535
1547
|
const isLocal = provider.isLocal;
|
|
1536
1548
|
const isTelegramMultiBot = state.botCount > 1 && state.channel === 'telegram';
|
|
1537
|
-
const relayPluginSpec = '
|
|
1549
|
+
const relayPluginSpec = 'openclaw-telegram-multibot-relay';
|
|
1538
1550
|
|
|
1539
1551
|
function buildRelayPluginInstallCommand(prefix) {
|
|
1540
1552
|
return `${prefix} plugins install ${relayPluginSpec} 2>/dev/null || true`;
|
|
@@ -1545,7 +1557,8 @@
|
|
|
1545
1557
|
}
|
|
1546
1558
|
|
|
1547
1559
|
function buildTelegramPostInstallChecklist() {
|
|
1548
|
-
const groupId = state.groupId || '';
|
|
1560
|
+
const groupId = state.groupId || '';
|
|
1561
|
+
const nativeProjectOpenClawRoot = `${projectDir.replace(/\\/g, '/')}/.openclaw`;
|
|
1549
1562
|
const botList = state.bots.slice(0, state.botCount).map((bot, idx) => `- **${bot?.name || `Bot ${idx + 1}`}**`).join('\n');
|
|
1550
1563
|
const isVi = lang === 'vi';
|
|
1551
1564
|
return isVi
|
|
@@ -1688,14 +1701,15 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1688
1701
|
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
|
1689
1702
|
channels: ch.channelConfig,
|
|
1690
1703
|
tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
|
|
1691
|
-
gateway: {
|
|
1692
|
-
port: 18791,
|
|
1693
|
-
mode: 'local',
|
|
1694
|
-
bind: '
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1704
|
+
gateway: {
|
|
1705
|
+
port: 18791,
|
|
1706
|
+
mode: 'local',
|
|
1707
|
+
bind: 'custom',
|
|
1708
|
+
customBindHost: '0.0.0.0',
|
|
1709
|
+
controlUi: {
|
|
1710
|
+
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
1711
|
+
},
|
|
1712
|
+
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
1699
1713
|
},
|
|
1700
1714
|
};
|
|
1701
1715
|
|
|
@@ -1793,13 +1807,14 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1793
1807
|
botToken: meta.token || '<your_bot_token>',
|
|
1794
1808
|
ackReaction: '👍',
|
|
1795
1809
|
}]));
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1810
|
+
const nativeOpenClawRoot = '.openclaw';
|
|
1811
|
+
clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
|
|
1812
|
+
id: meta.agentId,
|
|
1813
|
+
name: meta.name,
|
|
1814
|
+
workspace: `${nativeOpenClawRoot}/${meta.workspaceDir}`,
|
|
1815
|
+
agentDir: `${nativeOpenClawRoot}/agents/${meta.agentId}/agent`,
|
|
1816
|
+
model: { primary: state.config.model, fallbacks: [] },
|
|
1817
|
+
}));
|
|
1803
1818
|
clawConfig.bindings = multiBotAgentMetas.map((meta) => ({
|
|
1804
1819
|
agentId: meta.agentId,
|
|
1805
1820
|
match: { channel: 'telegram', accountId: meta.accountId },
|
|
@@ -1940,7 +1955,7 @@ model:
|
|
|
1940
1955
|
? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
|
|
1941
1956
|
: '';
|
|
1942
1957
|
// Patch config on every startup to keep gateway settings stable
|
|
1943
|
-
const patchCmd = `node -e \\"const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add(
|
|
1958
|
+
const patchCmd = `node -e \\"const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
|
|
1944
1959
|
// Auto-approve device pairing after gateway starts (required since v2026.3.x)
|
|
1945
1960
|
const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
|
|
1946
1961
|
const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${autoApproveCmd}${gatewayCmd}"`;
|
|
@@ -1951,7 +1966,7 @@ RUN apt-get update && apt-get install -y git curl${browserAptExtra} && rm -rf /v
|
|
|
1951
1966
|
|
|
1952
1967
|
|
|
1953
1968
|
ARG CACHEBUST=${Date.now()}
|
|
1954
|
-
RUN npm install -g openclaw@
|
|
1969
|
+
RUN npm install -g openclaw@2026.4.5 grammy${skillLines}${browserInstallLines}
|
|
1955
1970
|
RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';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) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"
|
|
1956
1971
|
WORKDIR /root/.openclaw
|
|
1957
1972
|
|
|
@@ -1969,8 +1984,8 @@ ${finalCmd}`;
|
|
|
1969
1984
|
|
|
1970
1985
|
// ─── Dynamic Smart Route Sync Script ────────────────────────────────────────
|
|
1971
1986
|
// Background loop inside 9Router container every 30s.
|
|
1972
|
-
//
|
|
1973
|
-
//
|
|
1987
|
+
// Sync against the 9Router API so smart-route matches the current
|
|
1988
|
+
// active provider set instead of stale db-only state.
|
|
1974
1989
|
const syncScript = `const fs=require('fs');const INTERVAL=30000;const p='/root/.9router/db.json';
|
|
1975
1990
|
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']};
|
|
1976
1991
|
console.log('[sync-combo] 9Router sync loop started...');
|
|
@@ -2759,13 +2774,14 @@ fi
|
|
|
2759
2774
|
id: botAgentId,
|
|
2760
2775
|
model: { primary: state.config.model, fallbacks: [] },
|
|
2761
2776
|
}];
|
|
2762
|
-
botConfig.gateway = {
|
|
2763
|
-
...(botConfig.gateway || {}),
|
|
2764
|
-
port: 18791,
|
|
2765
|
-
mode: 'local',
|
|
2766
|
-
bind: '
|
|
2767
|
-
|
|
2768
|
-
|
|
2777
|
+
botConfig.gateway = {
|
|
2778
|
+
...(botConfig.gateway || {}),
|
|
2779
|
+
port: 18791,
|
|
2780
|
+
mode: 'local',
|
|
2781
|
+
bind: 'custom',
|
|
2782
|
+
customBindHost: '0.0.0.0',
|
|
2783
|
+
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
2784
|
+
};
|
|
2769
2785
|
|
|
2770
2786
|
const botAgentYaml = `name: ${botAgentId}
|
|
2771
2787
|
description: "${botDesc}"
|
|
@@ -2967,41 +2983,62 @@ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
|
|
|
2967
2983
|
const ch = CHANNELS[state.channel];
|
|
2968
2984
|
const is9Router = !!(provider && provider.isProxy);
|
|
2969
2985
|
const isOllama = !!(provider && provider.isLocal);
|
|
2970
|
-
const hasBrowser = state.config.skills.includes('browser');
|
|
2971
|
-
const
|
|
2986
|
+
const hasBrowser = state.config.skills.includes('browser');
|
|
2987
|
+
const nativeSkillConfigs = state.config.skills
|
|
2988
|
+
.map((sid) => SKILLS.find((s) => s.id === sid))
|
|
2989
|
+
.filter((skill) => skill && skill.id !== 'scheduler' && skill.slug && skill.slug !== 'browser-automation');
|
|
2990
|
+
const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
|
|
2972
2991
|
const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
|
|
2973
2992
|
const projectDir = state.config.projectPath || '.';
|
|
2974
2993
|
|
|
2975
|
-
const allPlugins = [];
|
|
2994
|
+
const allPlugins = [];
|
|
2976
2995
|
if (ch && ch.pluginInstall) allPlugins.push(ch.pluginInstall);
|
|
2977
2996
|
state.config.plugins.forEach(function(pid) {
|
|
2978
2997
|
const p = PLUGINS.find((x) => x.id === pid);
|
|
2979
2998
|
if (p) allPlugins.push(p.package);
|
|
2980
2999
|
});
|
|
2981
|
-
if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
|
|
2982
|
-
const pluginCmd = allPlugins.length > 0 ? ('npm exec openclaw plugins install ' + allPlugins.join(' ')) : '';
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
const
|
|
2987
|
-
const
|
|
2988
|
-
const
|
|
2989
|
-
const
|
|
2990
|
-
const
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3000
|
+
if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
|
|
3001
|
+
const pluginCmd = allPlugins.length > 0 ? ('call npm exec -- openclaw plugins install ' + allPlugins.join(' ') + ' || goto :fail') : '';
|
|
3002
|
+
const nativeSkillInstallCmds = nativeSkillConfigs.map((skill) => `call openclaw skills install ${skill.slug} || echo Warning: Failed to install skill ${skill.slug}`);
|
|
3003
|
+
|
|
3004
|
+
function native9RouterSyncScriptContent() {
|
|
3005
|
+
return `const fs=require('fs');
|
|
3006
|
+
const path=require('path');
|
|
3007
|
+
const INTERVAL=30000;
|
|
3008
|
+
const p=path.join(process.env.DATA_DIR||'.9router','db.json');
|
|
3009
|
+
const ROUTER='http://localhost:20128';
|
|
3010
|
+
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']};
|
|
3011
|
+
console.log('[sync-combo] 9Router sync loop started...');
|
|
3012
|
+
const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.ok){console.log('[sync-combo] API not ready, retrying...');return;}const d=await res.json();const a=(d.connections||[]).filter(c=>c&&c.provider&&c.isActive!==false&&!c.disabled).map(c=>c.provider);let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catch{}if(!db.combos)db.combos=[];const removeSmartRoute=()=>{const next=db.combos.filter(x=>x.id!=='smart-route');if(next.length!==db.combos.length){db.combos=next;fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Removed smart-route (no active providers)');}};if(!a.length){removeSmartRoute();return;}const PREF=['openai','anthropic','claude-code','codex','cursor','github','cline','kimi','minimax','deepseek','glm','alicode','xai','mistral','kilo','kiro','iflow','qwen','gemini-cli','ollama'];a.sort((x,y)=>(PREF.indexOf(x)===-1?99:PREF.indexOf(x))-(PREF.indexOf(y)===-1?99:PREF.indexOf(y)));const m=a.flatMap(provider=>PM[provider]||[]);if(!m.length){removeSmartRoute();return;}const c={id:'smart-route',name:'smart-route',alias:'smart-route',models:m};const i=db.combos.findIndex(x=>x.id==='smart-route');if(i>=0){if(JSON.stringify(db.combos[i].models)!==JSON.stringify(c.models)){db.combos[i]=c;fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Updated smart-route: '+c.models.length+' models from: '+a.join(','));}}else{db.combos.push(c);fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Created smart-route: '+c.models.length+' models from: '+a.join(','));}}catch(e){console.log('[sync-combo] Error:',e.message);}};setTimeout(sync,5000);setInterval(sync,INTERVAL);`;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
function native9RouterServerEntryLookup() {
|
|
3016
|
+
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);\"";
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function windowsHiddenNodeLaunch(targetPath, extraEnv = {}) {
|
|
3020
|
+
function quotePowerShellSingle(value) {
|
|
3021
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
3022
|
+
}
|
|
3023
|
+
const envAssignments = Object.entries(extraEnv)
|
|
3024
|
+
.map(([key, value]) => `$env:${key}=${quotePowerShellSingle(String(value))}`)
|
|
3025
|
+
.join('; ');
|
|
3026
|
+
return `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${envAssignments ? `${envAssignments}; ` : ''}Start-Process -WindowStyle Hidden -FilePath (Get-Command node).Source -ArgumentList @('${targetPath.replace(/'/g, "''")}')"`;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// ─── Shared initializer (provider install) ───────────────────────────────
|
|
3030
|
+
function providerLines(arr, shell) {
|
|
3031
|
+
if (is9Router) {
|
|
3032
|
+
if (shell === 'bat') {
|
|
3033
|
+
arr.push('call npm install -g 9router || goto :fail');
|
|
3034
|
+
arr.push(`for /f "usebackq delims=" %%I in (\`${native9RouterServerEntryLookup()}\`) do set "NINE_ROUTER_ENTRY=%%I"`);
|
|
3035
|
+
arr.push(windowsHiddenNodeLaunch('%NINE_ROUTER_ENTRY%', { PORT: '20128', HOSTNAME: '0.0.0.0', DATA_DIR: '%DATA_DIR%' }));
|
|
3036
|
+
arr.push('timeout /t 5 /nobreak >nul');
|
|
3037
|
+
} else {
|
|
3038
|
+
arr.push('npm install -g 9router');
|
|
3039
|
+
arr.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
|
|
3040
|
+
arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 DATA_DIR="$PWD/.9router" node "$NINE_ROUTER_ENTRY" >/tmp/9router.log 2>&1 &');
|
|
3041
|
+
arr.push('nohup env DATA_DIR="$PWD/.9router" node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
|
|
3005
3042
|
arr.push('sleep 3');
|
|
3006
3043
|
}
|
|
3007
3044
|
} else if (isOllama) {
|
|
@@ -3062,7 +3099,7 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3062
3099
|
order: { ollama: ['ollama:default'] },
|
|
3063
3100
|
};
|
|
3064
3101
|
} else {
|
|
3065
|
-
const authProviderName = provider.isProxy ? '9router' : provider
|
|
3102
|
+
const authProviderName = provider.isProxy ? '9router' : state.config.provider;
|
|
3066
3103
|
const authProfileId = provider.isProxy ? '9router-proxy' : `${authProviderName}:default`;
|
|
3067
3104
|
const authKeyValue = provider.isProxy
|
|
3068
3105
|
? 'sk-no-key'
|
|
@@ -3118,8 +3155,8 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3118
3155
|
list: multiBotAgentMetas.map((meta) => ({
|
|
3119
3156
|
id: meta.agentId,
|
|
3120
3157
|
name: meta.name,
|
|
3121
|
-
workspace:
|
|
3122
|
-
agentDir:
|
|
3158
|
+
workspace: `${nativeProjectOpenClawRoot}/${meta.workspaceDir}`,
|
|
3159
|
+
agentDir: `${nativeProjectOpenClawRoot}/agents/${meta.agentId}/agent`,
|
|
3123
3160
|
model: { primary: state.config.model, fallbacks: [] },
|
|
3124
3161
|
})),
|
|
3125
3162
|
},
|
|
@@ -3161,14 +3198,15 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3161
3198
|
'telegram-multibot-relay': { enabled: true },
|
|
3162
3199
|
},
|
|
3163
3200
|
},
|
|
3164
|
-
gateway: {
|
|
3165
|
-
port: 18791,
|
|
3166
|
-
mode: 'local',
|
|
3167
|
-
bind: '
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3201
|
+
gateway: {
|
|
3202
|
+
port: 18791,
|
|
3203
|
+
mode: 'local',
|
|
3204
|
+
bind: 'custom',
|
|
3205
|
+
customBindHost: '0.0.0.0',
|
|
3206
|
+
controlUi: {
|
|
3207
|
+
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
3208
|
+
},
|
|
3209
|
+
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
3172
3210
|
},
|
|
3173
3211
|
};
|
|
3174
3212
|
return JSON.stringify(cfg, null, 2);
|
|
@@ -3254,44 +3292,116 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3254
3292
|
}
|
|
3255
3293
|
|
|
3256
3294
|
// ─── Per-bot openclaw.json (minimal — shared workspace) ──────────────────
|
|
3257
|
-
function botConfigContent(botIndex) {
|
|
3258
|
-
const bot = state.bots[botIndex] || {};
|
|
3259
|
-
const botName = bot.name || `Bot ${botIndex + 1}`;
|
|
3260
|
-
const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
3261
|
-
const basePort = 18791 + botIndex;
|
|
3262
|
-
const groupId = state.groupId || '';
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
+
function botConfigContent(botIndex) {
|
|
3296
|
+
const bot = state.bots[botIndex] || {};
|
|
3297
|
+
const botName = bot.name || `Bot ${botIndex + 1}`;
|
|
3298
|
+
const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
3299
|
+
const basePort = 18791 + botIndex;
|
|
3300
|
+
const groupId = state.groupId || '';
|
|
3301
|
+
const botProvider = PROVIDERS[bot.provider] || provider;
|
|
3302
|
+
const cfg = {
|
|
3303
|
+
meta: { lastTouchedVersion: '2026.3.24' },
|
|
3304
|
+
agents: {
|
|
3305
|
+
defaults: {
|
|
3306
|
+
model: { primary: bot.model || state.config.model },
|
|
3307
|
+
compaction: { mode: 'safeguard' },
|
|
3308
|
+
timeoutSeconds: botProvider.isLocal ? 900 : 120,
|
|
3309
|
+
...(botProvider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
|
|
3310
|
+
},
|
|
3311
|
+
list: [{ id: agentId, model: { primary: bot.model || state.config.model } }],
|
|
3312
|
+
},
|
|
3313
|
+
...(botProvider.isProxy ? {
|
|
3314
|
+
models: {
|
|
3315
|
+
mode: 'merge',
|
|
3316
|
+
providers: {
|
|
3317
|
+
'9router': {
|
|
3318
|
+
baseUrl: 'http://localhost:20128/v1',
|
|
3319
|
+
apiKey: 'sk-no-key',
|
|
3320
|
+
api: 'openai-completions',
|
|
3321
|
+
models: [
|
|
3322
|
+
{
|
|
3323
|
+
id: 'smart-route',
|
|
3324
|
+
name: 'Smart Proxy (Auto Route)',
|
|
3325
|
+
contextWindow: 200000,
|
|
3326
|
+
maxTokens: 8192,
|
|
3327
|
+
}
|
|
3328
|
+
]
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
} : {}),
|
|
3333
|
+
...(botProvider.isLocal ? {
|
|
3334
|
+
models: {
|
|
3335
|
+
providers: {
|
|
3336
|
+
ollama: {
|
|
3337
|
+
baseUrl: 'http://localhost:11434',
|
|
3338
|
+
apiKey: 'ollama-local',
|
|
3339
|
+
api: 'ollama',
|
|
3340
|
+
models: [
|
|
3341
|
+
{ id: selectedModel, name: selectedModel, contextWindow: 128000, maxTokens: 8192 }
|
|
3342
|
+
]
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
} : {}),
|
|
3347
|
+
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
|
3348
|
+
channels: {},
|
|
3349
|
+
tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
|
|
3350
|
+
gateway: {
|
|
3351
|
+
port: basePort,
|
|
3352
|
+
mode: 'local',
|
|
3353
|
+
bind: 'custom',
|
|
3354
|
+
customBindHost: '0.0.0.0',
|
|
3355
|
+
controlUi: {
|
|
3356
|
+
allowedOrigins: getGatewayAllowedOrigins(basePort),
|
|
3357
|
+
},
|
|
3358
|
+
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
3359
|
+
},
|
|
3360
|
+
};
|
|
3361
|
+
|
|
3362
|
+
if (hasBrowser) {
|
|
3363
|
+
cfg.browser = { enabled: true };
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
const skillEntries = {};
|
|
3367
|
+
state.config.skills.forEach((sid) => {
|
|
3368
|
+
const skill = SKILLS.find((s) => s.id === sid);
|
|
3369
|
+
if (!skill) return;
|
|
3370
|
+
if (skill.id === 'scheduler' || skill.slug === 'browser-automation' || !skill.slug) return;
|
|
3371
|
+
skillEntries[skill.slug] = { enabled: true };
|
|
3372
|
+
});
|
|
3373
|
+
if (Object.keys(skillEntries).length > 0) {
|
|
3374
|
+
cfg.skills = { entries: skillEntries };
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
if (state.channel === 'telegram') {
|
|
3378
|
+
cfg.channels.telegram = {
|
|
3379
|
+
enabled: true,
|
|
3380
|
+
dmPolicy: 'open',
|
|
3381
|
+
allowFrom: ['*'],
|
|
3382
|
+
};
|
|
3383
|
+
if (isMultiBot) {
|
|
3384
|
+
cfg.channels.telegram.groupPolicy = groupId ? 'allowlist' : 'open';
|
|
3385
|
+
cfg.channels.telegram.groupAllowFrom = ['*'];
|
|
3386
|
+
cfg.channels.telegram.groups = {
|
|
3387
|
+
[groupId || '*']: {
|
|
3388
|
+
enabled: true,
|
|
3389
|
+
requireMention: false,
|
|
3390
|
+
},
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
} else if (state.channel === 'zalo-personal') {
|
|
3394
|
+
cfg.channels.zalouser = {
|
|
3395
|
+
enabled: true,
|
|
3396
|
+
dmPolicy: 'open',
|
|
3397
|
+
autoReply: true,
|
|
3398
|
+
};
|
|
3399
|
+
} else if (state.channel === 'zalo-bot') {
|
|
3400
|
+
cfg.channels.zalo = { enabled: true, provider: 'official_account' };
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
return JSON.stringify(cfg, null, 2);
|
|
3404
|
+
}
|
|
3295
3405
|
|
|
3296
3406
|
function botAuthProfilesContent(botIndex) {
|
|
3297
3407
|
const bot = state.bots[botIndex] || {};
|
|
@@ -3311,7 +3421,7 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3311
3421
|
order: { ollama: ['ollama:default'] },
|
|
3312
3422
|
};
|
|
3313
3423
|
} else {
|
|
3314
|
-
const authProviderName = botProvider.isProxy ? '9router' :
|
|
3424
|
+
const authProviderName = botProvider.isProxy ? '9router' : (bot.provider || state.config.provider);
|
|
3315
3425
|
const authProfileId = botProvider.isProxy ? '9router-proxy' : `${authProviderName}:default`;
|
|
3316
3426
|
const authKeyValue = botProvider.isProxy
|
|
3317
3427
|
? 'sk-no-key'
|
|
@@ -3567,18 +3677,29 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3567
3677
|
.replace(/%/g, '%%');
|
|
3568
3678
|
}
|
|
3569
3679
|
|
|
3570
|
-
function appendBatWriteCommands(arr, files) {
|
|
3571
|
-
Object.entries(files).forEach(([relPath, content]) => {
|
|
3572
|
-
const winPath = relPath.replace(/\//g, '\\');
|
|
3573
|
-
const dir = winPath.substring(0, winPath.lastIndexOf('\\'));
|
|
3574
|
-
if (dir) arr.push(`if not exist "${dir}" mkdir "${dir}"`);
|
|
3680
|
+
function appendBatWriteCommands(arr, files) {
|
|
3681
|
+
Object.entries(files).forEach(([relPath, content]) => {
|
|
3682
|
+
const winPath = relPath.replace(/\//g, '\\');
|
|
3683
|
+
const dir = winPath.substring(0, winPath.lastIndexOf('\\'));
|
|
3684
|
+
if (dir) arr.push(`if not exist "${dir}" mkdir "${dir}"`);
|
|
3575
3685
|
arr.push(`> "${winPath}" (`);
|
|
3576
3686
|
content.split('\n').forEach((line) => {
|
|
3577
3687
|
arr.push(line.length ? `echo(${batEscapeEchoLine(line)}` : 'echo(');
|
|
3578
3688
|
});
|
|
3579
|
-
arr.push(')');
|
|
3580
|
-
});
|
|
3581
|
-
}
|
|
3689
|
+
arr.push(')');
|
|
3690
|
+
});
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
function mapWindowsNativeFiles(files) {
|
|
3694
|
+
return Object.fromEntries(Object.entries(files).map(([relPath, content]) => {
|
|
3695
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
3696
|
+
if (normalized === '.env') return ['%PROJECT_DIR%\\.env', content];
|
|
3697
|
+
if (normalized.startsWith('.openclaw/')) {
|
|
3698
|
+
return [`%OPENCLAW_HOME%\\${normalized.slice('.openclaw/'.length).replace(/\//g, '\\')}`, content];
|
|
3699
|
+
}
|
|
3700
|
+
return [`%PROJECT_DIR%\\${normalized.replace(/\//g, '\\')}`, content];
|
|
3701
|
+
}));
|
|
3702
|
+
}
|
|
3582
3703
|
|
|
3583
3704
|
let scriptContent = '';
|
|
3584
3705
|
let scriptName = '';
|
|
@@ -3587,33 +3708,82 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3587
3708
|
if (state.nativeOs === 'win') {
|
|
3588
3709
|
const isDocker = state.deployMode === 'docker';
|
|
3589
3710
|
scriptName = isDocker ? 'setup-openclaw-docker-win.bat' : 'setup-openclaw-win.bat';
|
|
3590
|
-
const lines = [
|
|
3591
|
-
'@echo off',
|
|
3592
|
-
'
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
'
|
|
3711
|
+
const lines = [
|
|
3712
|
+
'@echo off',
|
|
3713
|
+
'setlocal EnableExtensions',
|
|
3714
|
+
'chcp 65001 >nul',
|
|
3715
|
+
`set "PROJECT_DIR=${projectDir.replace(/\//g, '\\')}"`,
|
|
3716
|
+
'if not exist "%PROJECT_DIR%" mkdir "%PROJECT_DIR%"',
|
|
3717
|
+
'cd /d "%PROJECT_DIR%"',
|
|
3718
|
+
'set "OPENCLAW_HOME=%PROJECT_DIR%\\.openclaw"',
|
|
3719
|
+
'set "OPENCLAW_STATE_DIR=%PROJECT_DIR%\\.openclaw"',
|
|
3720
|
+
'set "DATA_DIR=%PROJECT_DIR%\\.9router"',
|
|
3721
|
+
'set "PATH=%APPDATA%\\npm;%PATH%"',
|
|
3722
|
+
`echo === OpenClaw Setup — Windows${isDocker ? ' Docker' : ' Native'} ===`,
|
|
3723
|
+
'echo.',
|
|
3724
|
+
'echo [1/5] Kiem tra Node.js...',
|
|
3596
3725
|
'where node >nul 2>&1 || (echo ERROR: Node.js chua cai! Tai tai: https://nodejs.org && pause && exit /b 1)',
|
|
3597
3726
|
'echo [2/5] Cai OpenClaw CLI...',
|
|
3598
|
-
'npm install -g openclaw@
|
|
3599
|
-
];
|
|
3600
|
-
providerLines(lines, 'bat');
|
|
3601
|
-
if (
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
lines.push('
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
lines.push('
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3727
|
+
'call npm install -g openclaw@2026.4.5 || goto :fail',
|
|
3728
|
+
];
|
|
3729
|
+
providerLines(lines, 'bat');
|
|
3730
|
+
if (hasBrowser) {
|
|
3731
|
+
lines.push('echo Cai Browser Automation runtime...');
|
|
3732
|
+
lines.push('call npm install -g agent-browser playwright || goto :fail');
|
|
3733
|
+
lines.push('call npx playwright install chromium || goto :fail');
|
|
3734
|
+
}
|
|
3735
|
+
if (nativeSkillInstallCmds.length > 0) {
|
|
3736
|
+
lines.push('echo Cai skills...');
|
|
3737
|
+
lines.push(...nativeSkillInstallCmds);
|
|
3738
|
+
}
|
|
3739
|
+
if (pluginCmd) { lines.push('echo Cai plugins...'); lines.push(pluginCmd); }
|
|
3740
|
+
lines.push('if not exist "%OPENCLAW_HOME%" mkdir "%OPENCLAW_HOME%"');
|
|
3741
|
+
lines.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
|
|
3742
|
+
|
|
3743
|
+
if (isMultiBot) {
|
|
3744
|
+
lines.push('echo [4/5] Tao runtime multi-agent dung chung...');
|
|
3745
|
+
appendBatWriteCommands(lines, mapWindowsNativeFiles(sharedNativeFileMap()));
|
|
3746
|
+
if (is9Router) lines.push(windowsHiddenNodeLaunch('%OPENCLAW_HOME%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
|
|
3747
|
+
lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
|
|
3748
|
+
lines.push('echo.');
|
|
3749
|
+
lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
|
|
3750
|
+
lines.push('echo Other reachable URLs: http://localhost:18791');
|
|
3751
|
+
lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
|
|
3752
|
+
if (is9Router) {
|
|
3753
|
+
lines.push('echo.');
|
|
3754
|
+
lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
|
|
3755
|
+
lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
|
|
3756
|
+
}
|
|
3757
|
+
lines.push('echo [5/5] Khoi dong gateway multi-bot...');
|
|
3758
|
+
lines.push('call openclaw gateway run');
|
|
3759
|
+
} else {
|
|
3760
|
+
lines.push('echo [4/5] Tao file cau hinh...');
|
|
3761
|
+
appendBatWriteCommands(lines, mapWindowsNativeFiles(botFiles(0)));
|
|
3762
|
+
if (is9Router) lines.push(windowsHiddenNodeLaunch('%OPENCLAW_HOME%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
|
|
3763
|
+
lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
|
|
3764
|
+
lines.push('echo.');
|
|
3765
|
+
lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
|
|
3766
|
+
lines.push('echo Other reachable URLs: http://localhost:18791');
|
|
3767
|
+
lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
|
|
3768
|
+
if (is9Router) {
|
|
3769
|
+
lines.push('echo.');
|
|
3770
|
+
lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
|
|
3771
|
+
lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
|
|
3772
|
+
}
|
|
3773
|
+
lines.push('echo [5/5] Khoi dong bot...');
|
|
3774
|
+
lines.push('call openclaw gateway run');
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
lines.push('goto :end');
|
|
3778
|
+
lines.push(':fail');
|
|
3779
|
+
lines.push('echo.');
|
|
3780
|
+
lines.push('echo Cai dat that bai. Kiem tra dong loi ngay phia tren.');
|
|
3781
|
+
lines.push('pause');
|
|
3782
|
+
lines.push('exit /b 1');
|
|
3783
|
+
lines.push(':end');
|
|
3784
|
+
lines.push('pause');
|
|
3785
|
+
lines.push('endlocal');
|
|
3786
|
+
scriptContent = lines.filter(Boolean).join('\r\n');
|
|
3617
3787
|
|
|
3618
3788
|
// ─── macOS .SH ───────────────────────────────────────────────────────────
|
|
3619
3789
|
} else if (state.nativeOs === 'linux') {
|
|
@@ -3642,18 +3812,24 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3642
3812
|
// ── macOS Native mode: same approach as Ubuntu but no PM2, no apt ────────
|
|
3643
3813
|
// Do NOT use 'npm config set prefix' on macOS — breaks Homebrew Node.
|
|
3644
3814
|
// Use export npm_config_prefix per-session + sudo fallback.
|
|
3645
|
-
const sh = [
|
|
3646
|
-
'#!/usr/bin/env bash', 'set -e',
|
|
3647
|
-
'echo "=== OpenClaw Setup \u2014 macOS Native ==="',
|
|
3648
|
-
'command -v node > /dev/null 2>&1 || { echo "ERROR: Node.js chua cai! https://nodejs.org"; exit 1; }',
|
|
3815
|
+
const sh = [
|
|
3816
|
+
'#!/usr/bin/env bash', 'set -e',
|
|
3817
|
+
'echo "=== OpenClaw Setup \u2014 macOS Native ==="',
|
|
3818
|
+
'command -v node > /dev/null 2>&1 || { echo "ERROR: Node.js chua cai! https://nodejs.org"; exit 1; }',
|
|
3649
3819
|
'# User-local npm prefix (Homebrew-safe — no global npmrc mutation)',
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3820
|
+
'mkdir -p "$HOME/.local/bin"',
|
|
3821
|
+
'export npm_config_prefix="$HOME/.local"',
|
|
3822
|
+
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3823
|
+
`PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
|
|
3824
|
+
'mkdir -p "$PROJECT_DIR"',
|
|
3825
|
+
'cd "$PROJECT_DIR"',
|
|
3826
|
+
'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
|
|
3827
|
+
'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
|
|
3828
|
+
'export DATA_DIR="$PROJECT_DIR/.9router"',
|
|
3829
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.zshrc"',
|
|
3830
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
3831
|
+
'# Install openclaw (user-local first, sudo fallback)',
|
|
3832
|
+
'npm install -g openclaw@2026.4.5 || sudo npm install -g openclaw@2026.4.5',
|
|
3657
3833
|
];
|
|
3658
3834
|
providerLines(sh, 'sh');
|
|
3659
3835
|
if (pluginCmd) sh.push(pluginCmd);
|
|
@@ -3672,20 +3848,26 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3672
3848
|
// ─── VPS/Ubuntu PM2 .SH ──────────────────────────────────────────────────
|
|
3673
3849
|
} else if (state.nativeOs === 'vps') {
|
|
3674
3850
|
scriptName = 'setup-openclaw-vps.sh';
|
|
3675
|
-
const vps = [
|
|
3676
|
-
'#!/usr/bin/env bash', 'set -e',
|
|
3851
|
+
const vps = [
|
|
3852
|
+
'#!/usr/bin/env bash', 'set -e',
|
|
3677
3853
|
`echo "=== OpenClaw Setup — Ubuntu/VPS${isMultiBot ? ` Multi-Bot (${state.botCount} bots)` : ''} ==="`,
|
|
3678
3854
|
'# Auto-install Node.js 20 LTS if missing',
|
|
3679
3855
|
'if ! command -v node > /dev/null 2>&1; then',
|
|
3680
3856
|
' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
|
|
3681
3857
|
' sudo apt-get install -y nodejs',
|
|
3682
3858
|
'fi',
|
|
3683
|
-
'mkdir -p "$HOME/.local/bin"',
|
|
3684
|
-
'npm config set prefix "$HOME/.local"',
|
|
3685
|
-
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3686
|
-
|
|
3687
|
-
'
|
|
3688
|
-
'
|
|
3859
|
+
'mkdir -p "$HOME/.local/bin"',
|
|
3860
|
+
'npm config set prefix "$HOME/.local"',
|
|
3861
|
+
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3862
|
+
`PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
|
|
3863
|
+
'mkdir -p "$PROJECT_DIR"',
|
|
3864
|
+
'cd "$PROJECT_DIR"',
|
|
3865
|
+
'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
|
|
3866
|
+
'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
|
|
3867
|
+
'export DATA_DIR="$PROJECT_DIR/.9router"',
|
|
3868
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
|
|
3869
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
3870
|
+
'npm install -g openclaw@2026.4.5 pm2@latest',
|
|
3689
3871
|
];
|
|
3690
3872
|
providerLines(vps, 'sh');
|
|
3691
3873
|
if (pluginCmd) vps.push(pluginCmd);
|
|
@@ -3695,7 +3877,8 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3695
3877
|
appendShWriteCommands(vps, sharedNativeFileMap());
|
|
3696
3878
|
vps.push('echo "--- Starting shared gateway via PM2 ---"');
|
|
3697
3879
|
if (is9Router) {
|
|
3698
|
-
vps.push(
|
|
3880
|
+
vps.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
|
|
3881
|
+
vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$NINE_ROUTER_ENTRY" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
|
|
3699
3882
|
vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
|
|
3700
3883
|
}
|
|
3701
3884
|
vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
|
|
@@ -3708,7 +3891,8 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3708
3891
|
} else {
|
|
3709
3892
|
appendShWriteCommands(vps, botFiles(0));
|
|
3710
3893
|
if (is9Router) {
|
|
3711
|
-
vps.push(
|
|
3894
|
+
vps.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
|
|
3895
|
+
vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$NINE_ROUTER_ENTRY" --name openclaw-9router --interpreter "$(command -v node)"');
|
|
3712
3896
|
vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
|
|
3713
3897
|
}
|
|
3714
3898
|
vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
|
|
@@ -3727,12 +3911,18 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3727
3911
|
' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
|
|
3728
3912
|
' sudo apt-get install -y nodejs',
|
|
3729
3913
|
'fi',
|
|
3730
|
-
'mkdir -p "$HOME/.local/bin"',
|
|
3731
|
-
'npm config set prefix "$HOME/.local"',
|
|
3732
|
-
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3733
|
-
|
|
3734
|
-
'
|
|
3735
|
-
'
|
|
3914
|
+
'mkdir -p "$HOME/.local/bin"',
|
|
3915
|
+
'npm config set prefix "$HOME/.local"',
|
|
3916
|
+
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3917
|
+
`PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
|
|
3918
|
+
'mkdir -p "$PROJECT_DIR"',
|
|
3919
|
+
'cd "$PROJECT_DIR"',
|
|
3920
|
+
'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
|
|
3921
|
+
'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
|
|
3922
|
+
'export DATA_DIR="$PROJECT_DIR/.9router"',
|
|
3923
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
|
|
3924
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
3925
|
+
'npm install -g openclaw@2026.4.5',
|
|
3736
3926
|
];
|
|
3737
3927
|
providerLines(lnx, 'sh');
|
|
3738
3928
|
if (pluginCmd) lnx.push(pluginCmd);
|
|
@@ -3766,7 +3956,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3766
3956
|
if (stepsList) {
|
|
3767
3957
|
const steps = [];
|
|
3768
3958
|
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)');
|
|
3769
|
-
steps.push(isVi ? '📦 Cài OpenClaw CLI (<code>npm install -g openclaw@
|
|
3959
|
+
steps.push(isVi ? '📦 Cài OpenClaw CLI (<code>npm install -g openclaw@2026.4.5</code>)' : '📦 Install OpenClaw CLI (<code>npm install -g openclaw@2026.4.5</code>)');
|
|
3770
3960
|
if (is9Router) {
|
|
3771
3961
|
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');
|
|
3772
3962
|
} else if (isOllama) {
|
|
@@ -3791,10 +3981,12 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3791
3981
|
|
|
3792
3982
|
|
|
3793
3983
|
|
|
3794
|
-
window.downloadNativeScript = function() {
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
const
|
|
3984
|
+
window.downloadNativeScript = function() {
|
|
3985
|
+
// Regenerate output first so the downloaded script always matches the latest wizard state.
|
|
3986
|
+
generateOutput();
|
|
3987
|
+
const script = window._nativeScript;
|
|
3988
|
+
if (!script) return;
|
|
3989
|
+
const blob = new Blob([script.content], { type: 'text/plain;charset=utf-8' });
|
|
3798
3990
|
const url = URL.createObjectURL(blob);
|
|
3799
3991
|
const a = document.createElement('a');
|
|
3800
3992
|
a.href = url; a.download = script.name; a.style.display = 'none';
|