create-openclaw-bot 5.1.14 → 5.2.0
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 +185 -158
- package/CHANGELOG.vi.md +279 -252
- package/README.md +12 -11
- package/README.vi.md +12 -11
- package/cli.js +416 -233
- package/package.json +1 -1
- package/setup.js +636 -318
- package/style.css +168 -37
- package/tests/smoke-cli-logic.mjs +123 -24
- package/upgrade.ps1 +90 -0
- package/upgrade.sh +93 -0
package/setup.js
CHANGED
|
@@ -66,9 +66,10 @@
|
|
|
66
66
|
|
|
67
67
|
// ========== AI Providers & Models ==========
|
|
68
68
|
const PROVIDERS = {
|
|
69
|
-
google: {
|
|
70
|
-
name: 'Google Gemini',
|
|
71
|
-
logo: LOGO.gemini,
|
|
69
|
+
google: {
|
|
70
|
+
name: 'Google Gemini',
|
|
71
|
+
logo: LOGO.gemini,
|
|
72
|
+
supportsEmbeddings: true,
|
|
72
73
|
envKey: 'GOOGLE_API_KEY',
|
|
73
74
|
envLabel: 'Google AI API Key',
|
|
74
75
|
envLink: 'https://aistudio.google.com/apikey',
|
|
@@ -80,9 +81,10 @@
|
|
|
80
81
|
{ id: 'google/gemini-3-flash', name: 'Gemini 3 Flash', descVi: 'Thế hệ mới, cực nhanh', descEn: 'Next gen, extremely fast', badge: '🆓 Free' },
|
|
81
82
|
],
|
|
82
83
|
},
|
|
83
|
-
anthropic: {
|
|
84
|
-
name: 'Anthropic Claude',
|
|
85
|
-
logo: LOGO.anthropic,
|
|
84
|
+
anthropic: {
|
|
85
|
+
name: 'Anthropic Claude',
|
|
86
|
+
logo: LOGO.anthropic,
|
|
87
|
+
supportsEmbeddings: false,
|
|
86
88
|
envKey: 'ANTHROPIC_API_KEY',
|
|
87
89
|
envLabel: 'Anthropic API Key',
|
|
88
90
|
envLink: 'https://console.anthropic.com/settings/keys',
|
|
@@ -94,9 +96,10 @@
|
|
|
94
96
|
{ id: 'anthropic/claude-haiku-3.5', name: 'Claude Haiku 3.5', descVi: 'Nhanh, rẻ nhất', descEn: 'Fastest, cheapest', badge: '💰 Paid' },
|
|
95
97
|
],
|
|
96
98
|
},
|
|
97
|
-
openai: {
|
|
98
|
-
name: 'OpenAI / Codex',
|
|
99
|
-
logo: LOGO.openai,
|
|
99
|
+
openai: {
|
|
100
|
+
name: 'OpenAI / Codex',
|
|
101
|
+
logo: LOGO.openai,
|
|
102
|
+
supportsEmbeddings: true,
|
|
100
103
|
envKey: 'OPENAI_API_KEY',
|
|
101
104
|
envLabel: 'OpenAI API Key',
|
|
102
105
|
envLink: 'https://platform.openai.com/api-keys',
|
|
@@ -109,9 +112,10 @@
|
|
|
109
112
|
{ id: 'openai/codex-mini', name: 'Codex Mini', descVi: 'Chuyên code, agent', descEn: 'Optimized for code/agents', badge: '💰 Paid' },
|
|
110
113
|
],
|
|
111
114
|
},
|
|
112
|
-
openrouter: {
|
|
113
|
-
name: 'OpenRouter',
|
|
114
|
-
logo: LOGO.openrouter,
|
|
115
|
+
openrouter: {
|
|
116
|
+
name: 'OpenRouter',
|
|
117
|
+
logo: LOGO.openrouter,
|
|
118
|
+
supportsEmbeddings: false,
|
|
115
119
|
envKey: 'OPENROUTER_API_KEY',
|
|
116
120
|
envLabel: 'OpenRouter API Key',
|
|
117
121
|
envLink: 'https://openrouter.ai/keys',
|
|
@@ -123,9 +127,10 @@
|
|
|
123
127
|
{ id: 'openrouter/qwen/qwen3-coder:free', name: 'Qwen 3 Coder', descVi: 'Alibaba, code, miễn phí', descEn: 'Alibaba, code, free', badge: '🆓 Free' },
|
|
124
128
|
],
|
|
125
129
|
},
|
|
126
|
-
ollama: {
|
|
127
|
-
name: 'Ollama (Local)',
|
|
128
|
-
logo: LOGO.ollama,
|
|
130
|
+
ollama: {
|
|
131
|
+
name: 'Ollama (Local)',
|
|
132
|
+
logo: LOGO.ollama,
|
|
133
|
+
supportsEmbeddings: true,
|
|
129
134
|
envKey: 'OLLAMA_HOST',
|
|
130
135
|
envLabel: 'Ollama Host URL',
|
|
131
136
|
envLink: 'https://ollama.com',
|
|
@@ -143,10 +148,11 @@
|
|
|
143
148
|
{ id: 'ollama/gemma3:12b', name: 'Gemma 3 12B', descVi: 'Google, tiếng Việt tốt', descEn: 'Google, great logic', badge: '🏠 Local' },
|
|
144
149
|
],
|
|
145
150
|
},
|
|
146
|
-
'9router': {
|
|
147
|
-
name: '9Router (Proxy)',
|
|
148
|
-
logo: null,
|
|
149
|
-
logoEmoji: '🔀',
|
|
151
|
+
'9router': {
|
|
152
|
+
name: '9Router (Proxy)',
|
|
153
|
+
logo: null,
|
|
154
|
+
logoEmoji: '🔀',
|
|
155
|
+
supportsEmbeddings: false,
|
|
150
156
|
envKey: null,
|
|
151
157
|
envLabel: null,
|
|
152
158
|
envLink: 'https://github.com/decolua/9router',
|
|
@@ -161,14 +167,14 @@
|
|
|
161
167
|
|
|
162
168
|
// ========== Available Plugins (npm packages — runtime/channel extensions) ==========
|
|
163
169
|
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
|
-
},
|
|
170
|
+
{
|
|
171
|
+
id: 'telegram-multibot-relay',
|
|
172
|
+
name: 'Telegram Multi-Bot Relay',
|
|
173
|
+
icon: '🤝',
|
|
174
|
+
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',
|
|
175
|
+
package: 'openclaw-telegram-multibot-relay',
|
|
176
|
+
hidden: true, // hidden in UI, auto-selected programmatically
|
|
177
|
+
},
|
|
172
178
|
{
|
|
173
179
|
id: 'voice-call',
|
|
174
180
|
name: 'Voice Call',
|
|
@@ -200,7 +206,7 @@
|
|
|
200
206
|
];
|
|
201
207
|
|
|
202
208
|
// ========== Available Skills (ClawHub registry — agent capabilities) ==========
|
|
203
|
-
const SKILLS = [
|
|
209
|
+
const SKILLS = [
|
|
204
210
|
{
|
|
205
211
|
id: 'browser',
|
|
206
212
|
name: 'Browser Automation ⭐(Khuyên dùng)',
|
|
@@ -280,7 +286,73 @@
|
|
|
280
286
|
noteVi: 'Cần Slack Bot Token', noteEn: 'Requires Slack Bot Token',
|
|
281
287
|
envVars: ['SLACK_BOT_TOKEN=<your_slack_bot_token>'],
|
|
282
288
|
},
|
|
283
|
-
];
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
function providerSupportsMemoryEmbeddings(providerKey) {
|
|
292
|
+
const provider = PROVIDERS[providerKey];
|
|
293
|
+
if (!provider) return false;
|
|
294
|
+
return !!provider.supportsEmbeddings;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getSkillDisplayName(skill, providerKey, lang) {
|
|
298
|
+
const normalizedName = String(skill.name || '')
|
|
299
|
+
.replace(/\s*[⭐🌟]\s*\((Khuyên dùng|Recommended)\)\s*/gi, '')
|
|
300
|
+
.replace(/\s*[⭐🌟]\s*(Khuyên dùng|Recommended)\s*/gi, '')
|
|
301
|
+
.trim();
|
|
302
|
+
if (skill.id === 'memory') {
|
|
303
|
+
return 'Long-term Memory';
|
|
304
|
+
}
|
|
305
|
+
return normalizedName;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function getSkillBadge(skill, providerKey, lang) {
|
|
309
|
+
const isVi = lang !== 'en';
|
|
310
|
+
if (skill.id === 'memory' && providerSupportsMemoryEmbeddings(providerKey)) {
|
|
311
|
+
return {
|
|
312
|
+
text: isVi ? 'Khuyên dùng' : 'Recommended',
|
|
313
|
+
className: 'plugin-card__badge plugin-card__badge--recommended'
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (skill.id === 'browser' || skill.id === 'scheduler') {
|
|
317
|
+
return {
|
|
318
|
+
text: isVi ? 'Khuyên dùng' : 'Recommended',
|
|
319
|
+
className: 'plugin-card__badge plugin-card__badge--recommended'
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getSkillExtraNote(skill, providerKey, lang) {
|
|
326
|
+
const isVi = lang !== 'en';
|
|
327
|
+
const baseNote = isVi ? (skill.noteVi || skill.note || '') : (skill.noteEn || skill.note || '');
|
|
328
|
+
if (skill.id !== 'memory' || providerSupportsMemoryEmbeddings(providerKey)) {
|
|
329
|
+
return baseNote;
|
|
330
|
+
}
|
|
331
|
+
const providerName = PROVIDERS[providerKey]?.name || providerKey;
|
|
332
|
+
const memoryNote = isVi
|
|
333
|
+
? `Provider hiện tại (${providerName}) chưa có đường embeddings được xác nhận trong wizard, nên memory search sẽ không được gắn khuyên dùng.`
|
|
334
|
+
: `The current provider (${providerName}) does not have a confirmed embeddings path in the wizard, so memory search is not marked recommended.`;
|
|
335
|
+
return baseNote ? `${baseNote} ${memoryNote}` : memoryNote;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function escapeHtml(text) {
|
|
339
|
+
return String(text || '')
|
|
340
|
+
.replace(/&/g, '&')
|
|
341
|
+
.replace(/</g, '<')
|
|
342
|
+
.replace(/>/g, '>')
|
|
343
|
+
.replace(/"/g, '"')
|
|
344
|
+
.replace(/'/g, ''');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function getSkillTooltipContent(skill, providerKey, lang) {
|
|
348
|
+
const desc = lang === 'vi' ? (skill.descVi || skill.desc || '') : (skill.descEn || skill.desc || '');
|
|
349
|
+
const note = getSkillExtraNote(skill, providerKey, lang);
|
|
350
|
+
return [desc, note].filter(Boolean).join('\n\n');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function getPluginTooltipContent(plugin, lang) {
|
|
354
|
+
return lang === 'vi' ? (plugin.descVi || plugin.desc || '') : (plugin.descEn || plugin.desc || '');
|
|
355
|
+
}
|
|
284
356
|
|
|
285
357
|
// ========== Channel definitions ==========
|
|
286
358
|
const CHANNELS = {
|
|
@@ -1049,82 +1121,132 @@
|
|
|
1049
1121
|
}).join('');
|
|
1050
1122
|
}
|
|
1051
1123
|
|
|
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
|
-
|
|
1124
|
+
window.__selectProvider = function (key) {
|
|
1125
|
+
state.config.provider = key;
|
|
1126
|
+
const p = PROVIDERS[key];
|
|
1127
|
+
state.config.model = p.models[0].id;
|
|
1128
|
+
if (state.bots[state.activeBotIndex]) {
|
|
1129
|
+
state.bots[state.activeBotIndex].provider = key;
|
|
1130
|
+
state.bots[state.activeBotIndex].model = p.models[0].id;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Highlight card
|
|
1134
|
+
document.querySelectorAll('.provider-card').forEach((c) => c.classList.remove('provider-card--selected'));
|
|
1135
|
+
document.querySelector(`.provider-card[data-provider="${key}"]`)?.classList.add('provider-card--selected');
|
|
1060
1136
|
|
|
1061
1137
|
// Update model dropdown
|
|
1062
1138
|
const modelSelect = document.getElementById('cfg-model');
|
|
1063
|
-
if (modelSelect) {
|
|
1064
|
-
modelSelect.innerHTML = p.models.map((m) =>
|
|
1065
|
-
`<option value="${m.id}">${m.name} — ${(() => { const l=document.getElementById('cfg-language')?.value||'vi'; return l==='vi'?(m.descVi||m.desc):(m.descEn||m.desc); })()} ${(() => { const l=document.getElementById('cfg-language')?.value||'vi'; return l==='vi'?(m.badgeVi||m.badge):(m.badgeEn||m.badge); })()}</option>`
|
|
1066
|
-
).join('');
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1139
|
+
if (modelSelect) {
|
|
1140
|
+
modelSelect.innerHTML = p.models.map((m) =>
|
|
1141
|
+
`<option value="${m.id}">${m.name} — ${(() => { const l=document.getElementById('cfg-language')?.value||'vi'; return l==='vi'?(m.descVi||m.desc):(m.descEn||m.desc); })()} ${(() => { const l=document.getElementById('cfg-language')?.value||'vi'; return l==='vi'?(m.badgeVi||m.badge):(m.badgeEn||m.badge); })()}</option>`
|
|
1142
|
+
).join('');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
renderPluginGrid();
|
|
1146
|
+
};
|
|
1069
1147
|
|
|
1070
1148
|
function renderPluginGrid() {
|
|
1071
1149
|
const lang = document.getElementById('cfg-language')?.value || 'vi';
|
|
1072
1150
|
|
|
1073
1151
|
// Skills grid (agent capabilities from ClawHub)
|
|
1074
|
-
const skillGrid = document.getElementById('plugin-grid');
|
|
1075
|
-
if (skillGrid) {
|
|
1076
|
-
skillGrid.innerHTML = SKILLS.map((s) => `
|
|
1077
|
-
<label class="plugin-card" data-skill="${s.id}">
|
|
1078
|
-
<input type="checkbox" class="plugin-checkbox" value="${s.id}" onchange="window.__toggleSkill('${s.id}', this.checked)">
|
|
1079
|
-
<div class="plugin-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1152
|
+
const skillGrid = document.getElementById('plugin-grid');
|
|
1153
|
+
if (skillGrid) {
|
|
1154
|
+
skillGrid.innerHTML = SKILLS.map((s) => `
|
|
1155
|
+
<label class="plugin-card ${state.config.skills.includes(s.id) ? 'plugin-card--selected' : ''}" data-skill="${s.id}">
|
|
1156
|
+
<input type="checkbox" class="plugin-checkbox" value="${s.id}" ${state.config.skills.includes(s.id) ? 'checked' : ''} onchange="window.__toggleSkill('${s.id}', this.checked)">
|
|
1157
|
+
<div class="plugin-card__info">
|
|
1158
|
+
<div class="plugin-card__topline">
|
|
1159
|
+
<div class="plugin-card__titleline">
|
|
1160
|
+
<div class="plugin-card__icon">${s.icon}</div>
|
|
1161
|
+
<div class="plugin-card__name">${getSkillDisplayName(s, state.config.provider, lang)}</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
<span class="toggle-switch plugin-card__switch">
|
|
1164
|
+
<input type="checkbox" tabindex="-1" aria-hidden="true" ${state.config.skills.includes(s.id) ? 'checked' : ''}>
|
|
1165
|
+
<span class="toggle-slider"></span>
|
|
1166
|
+
</span>
|
|
1167
|
+
</div>
|
|
1168
|
+
<div class="plugin-card__subline">
|
|
1169
|
+
<div class="plugin-card__hint-slot">
|
|
1170
|
+
${(() => {
|
|
1171
|
+
const tooltip = getSkillTooltipContent(s, state.config.provider, lang);
|
|
1172
|
+
return tooltip
|
|
1173
|
+
? `<span class="plugin-card__hint" tabindex="0" role="note" aria-label="${escapeHtml(tooltip)}">ⓘ<span class="plugin-card__tooltip">${escapeHtml(tooltip)}</span></span>`
|
|
1174
|
+
: '<span class="plugin-card__hint plugin-card__hint--placeholder" aria-hidden="true"></span>';
|
|
1175
|
+
})()}
|
|
1176
|
+
</div>
|
|
1177
|
+
<div class="plugin-card__badge-slot">
|
|
1178
|
+
${(() => {
|
|
1179
|
+
const badge = getSkillBadge(s, state.config.provider, lang);
|
|
1180
|
+
return badge ? `<span class="${badge.className}">${badge.text}</span>` : '<span class="plugin-card__badge plugin-card__badge--placeholder" aria-hidden="true"></span>';
|
|
1181
|
+
})()}
|
|
1182
|
+
</div>
|
|
1183
|
+
</div>
|
|
1184
|
+
</div>
|
|
1185
|
+
</label>
|
|
1186
|
+
`).join('');
|
|
1187
|
+
}
|
|
1089
1188
|
|
|
1090
1189
|
// Plugins grid (npm packages — extra channels/extensions)
|
|
1091
1190
|
// Filter out hidden plugins from user-facing grid
|
|
1092
1191
|
const visiblePlugins = PLUGINS.filter((p) => !p.hidden);
|
|
1093
1192
|
const pluginGrid = document.getElementById('extra-plugin-grid');
|
|
1094
|
-
if (pluginGrid) {
|
|
1095
|
-
pluginGrid.innerHTML = visiblePlugins.map((p) => `
|
|
1096
|
-
<label class="plugin-card" data-plugin="${p.id}">
|
|
1097
|
-
<input type="checkbox" class="plugin-checkbox" value="${p.id}" onchange="window.__togglePlugin('${p.id}', this.checked)">
|
|
1098
|
-
<div class="plugin-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1193
|
+
if (pluginGrid) {
|
|
1194
|
+
pluginGrid.innerHTML = visiblePlugins.map((p) => `
|
|
1195
|
+
<label class="plugin-card ${state.config.plugins.includes(p.id) ? 'plugin-card--selected' : ''}" data-plugin="${p.id}">
|
|
1196
|
+
<input type="checkbox" class="plugin-checkbox" value="${p.id}" ${state.config.plugins.includes(p.id) ? 'checked' : ''} onchange="window.__togglePlugin('${p.id}', this.checked)">
|
|
1197
|
+
<div class="plugin-card__info">
|
|
1198
|
+
<div class="plugin-card__topline">
|
|
1199
|
+
<div class="plugin-card__titleline">
|
|
1200
|
+
<div class="plugin-card__icon">${p.icon}</div>
|
|
1201
|
+
<div class="plugin-card__name">${p.name}</div>
|
|
1202
|
+
</div>
|
|
1203
|
+
<span class="toggle-switch plugin-card__switch">
|
|
1204
|
+
<input type="checkbox" tabindex="-1" aria-hidden="true" ${state.config.plugins.includes(p.id) ? 'checked' : ''}>
|
|
1205
|
+
<span class="toggle-slider"></span>
|
|
1206
|
+
</span>
|
|
1207
|
+
</div>
|
|
1208
|
+
<div class="plugin-card__subline">
|
|
1209
|
+
<div class="plugin-card__hint-slot">
|
|
1210
|
+
${(() => {
|
|
1211
|
+
const tooltip = getPluginTooltipContent(p, lang);
|
|
1212
|
+
return tooltip
|
|
1213
|
+
? `<span class="plugin-card__hint" tabindex="0" role="note" aria-label="${escapeHtml(tooltip)}">ⓘ<span class="plugin-card__tooltip">${escapeHtml(tooltip)}</span></span>`
|
|
1214
|
+
: '<span class="plugin-card__hint plugin-card__hint--placeholder" aria-hidden="true"></span>';
|
|
1215
|
+
})()}
|
|
1216
|
+
</div>
|
|
1217
|
+
<div class="plugin-card__badge-slot">
|
|
1218
|
+
<span class="plugin-card__badge plugin-card__badge--placeholder" aria-hidden="true"></span>
|
|
1219
|
+
</div>
|
|
1220
|
+
</div>
|
|
1221
|
+
</div>
|
|
1222
|
+
</label>
|
|
1223
|
+
`).join('');
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1118
1226
|
|
|
1119
|
-
window.
|
|
1120
|
-
if (checked && !state.config.
|
|
1121
|
-
state.config.
|
|
1122
|
-
} else {
|
|
1123
|
-
state.config.
|
|
1124
|
-
}
|
|
1125
|
-
document.querySelector(`.plugin-card[data-
|
|
1126
|
-
|
|
1127
|
-
|
|
1227
|
+
window.__toggleSkill = function (id, checked) {
|
|
1228
|
+
if (checked && !state.config.skills.includes(id)) {
|
|
1229
|
+
state.config.skills.push(id);
|
|
1230
|
+
} else {
|
|
1231
|
+
state.config.skills = state.config.skills.filter((s) => s !== id);
|
|
1232
|
+
}
|
|
1233
|
+
const card = document.querySelector(`.plugin-card[data-skill="${id}"]`);
|
|
1234
|
+
card?.classList.toggle('plugin-card--selected', checked);
|
|
1235
|
+
const switchInput = card?.querySelector('.plugin-card__switch input');
|
|
1236
|
+
if (switchInput) switchInput.checked = checked;
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
window.__togglePlugin = function (id, checked) {
|
|
1240
|
+
if (checked && !state.config.plugins.includes(id)) {
|
|
1241
|
+
state.config.plugins.push(id);
|
|
1242
|
+
} else {
|
|
1243
|
+
state.config.plugins = state.config.plugins.filter((p) => p !== id);
|
|
1244
|
+
}
|
|
1245
|
+
const card = document.querySelector(`.plugin-card[data-plugin="${id}"]`);
|
|
1246
|
+
card?.classList.toggle('plugin-card--selected', checked);
|
|
1247
|
+
const switchInput = card?.querySelector('.plugin-card__switch input');
|
|
1248
|
+
if (switchInput) switchInput.checked = checked;
|
|
1249
|
+
};
|
|
1128
1250
|
|
|
1129
1251
|
function bindFormEvents() {
|
|
1130
1252
|
// Language change is now handled by __selectLang
|
|
@@ -1246,36 +1368,44 @@
|
|
|
1246
1368
|
renderBotTabBar();
|
|
1247
1369
|
}
|
|
1248
1370
|
|
|
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';
|
|
1371
|
+
function saveFormData() {
|
|
1372
|
+
state.config.botName = document.getElementById('cfg-name')?.value || state.config.botName || 'Chat Bot';
|
|
1373
|
+
state.config.description = document.getElementById('cfg-desc')?.value || state.config.description || 'Personal AI assistant';
|
|
1374
|
+
state.config.emoji = document.getElementById('cfg-emoji')?.value || state.config.emoji || '🤖';
|
|
1375
|
+
state.config.model = document.getElementById('cfg-model')?.value || state.config.model || 'google/gemini-2.5-flash';
|
|
1254
1376
|
state.config.language = document.getElementById('cfg-language')?.value || state.config.language || 'vi';
|
|
1255
1377
|
state.config.systemPrompt = document.getElementById('cfg-prompt')?.value || state.config.systemPrompt || DEFAULT_PROMPTS['vi'];
|
|
1256
1378
|
state.config.userInfo = document.getElementById('cfg-user-info')?.value?.trim() || state.config.userInfo || '';
|
|
1257
1379
|
state.config.securityRules = document.getElementById('cfg-security')?.value || state.config.securityRules || DEFAULT_SECURITY_RULES['vi'];
|
|
1258
1380
|
// 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
|
-
|
|
1381
|
+
const tabName = document.getElementById('cfg-bot-tab-name')?.value?.trim();
|
|
1382
|
+
if (tabName && state.bots[0]) state.bots[0].name = tabName;
|
|
1383
|
+
else if (state.config.botName && state.bots[0] && !state.bots[0].name) {
|
|
1384
|
+
state.bots[0].name = state.config.botName;
|
|
1385
|
+
}
|
|
1386
|
+
if (state.bots[state.activeBotIndex]) {
|
|
1387
|
+
state.bots[state.activeBotIndex].provider = state.config.provider;
|
|
1388
|
+
state.bots[state.activeBotIndex].model = state.config.model;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1265
1391
|
|
|
1266
1392
|
// 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
|
-
|
|
1393
|
+
function saveCredentials() {
|
|
1394
|
+
const botTokenEl = document.getElementById('key-bot-token');
|
|
1395
|
+
const apiKeyEl = document.getElementById('key-api-key');
|
|
1396
|
+
const pathEl = document.getElementById('cfg-project-path');
|
|
1397
|
+
if (botTokenEl) state.config.botToken = botTokenEl.value;
|
|
1398
|
+
if (apiKeyEl) state.config.apiKey = apiKeyEl.value;
|
|
1399
|
+
if (pathEl) state.config.projectPath = pathEl.value;
|
|
1400
|
+
if (state.botCount <= 1 && state.bots[state.activeBotIndex]) {
|
|
1401
|
+
if (botTokenEl) state.bots[state.activeBotIndex].token = botTokenEl.value;
|
|
1402
|
+
if (apiKeyEl) state.bots[state.activeBotIndex].apiKey = apiKeyEl.value;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Also save multi-bot tokens individually
|
|
1406
|
+
if (state.botCount > 1) {
|
|
1407
|
+
for (let i = 0; i < state.botCount; i++) {
|
|
1408
|
+
const el = document.getElementById(`key-bot-token-${i}`);
|
|
1279
1409
|
if (el && state.bots[i]) state.bots[i].token = el.value;
|
|
1280
1410
|
}
|
|
1281
1411
|
}
|
|
@@ -1534,7 +1664,8 @@
|
|
|
1534
1664
|
const is9Router = provider.isProxy;
|
|
1535
1665
|
const isLocal = provider.isLocal;
|
|
1536
1666
|
const isTelegramMultiBot = state.botCount > 1 && state.channel === 'telegram';
|
|
1537
|
-
const relayPluginSpec = '
|
|
1667
|
+
const relayPluginSpec = 'openclaw-telegram-multibot-relay';
|
|
1668
|
+
const openClawRuntimePackages = 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
|
|
1538
1669
|
|
|
1539
1670
|
function buildRelayPluginInstallCommand(prefix) {
|
|
1540
1671
|
return `${prefix} plugins install ${relayPluginSpec} 2>/dev/null || true`;
|
|
@@ -1545,7 +1676,8 @@
|
|
|
1545
1676
|
}
|
|
1546
1677
|
|
|
1547
1678
|
function buildTelegramPostInstallChecklist() {
|
|
1548
|
-
const groupId = state.groupId || '';
|
|
1679
|
+
const groupId = state.groupId || '';
|
|
1680
|
+
const nativeProjectOpenClawRoot = `${projectDir.replace(/\\/g, '/')}/.openclaw`;
|
|
1549
1681
|
const botList = state.bots.slice(0, state.botCount).map((bot, idx) => `- **${bot?.name || `Bot ${idx + 1}`}**`).join('\n');
|
|
1550
1682
|
const isVi = lang === 'vi';
|
|
1551
1683
|
return isVi
|
|
@@ -1680,22 +1812,24 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1680
1812
|
timeoutSeconds: isLocal ? 900 : 120,
|
|
1681
1813
|
...(isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
|
|
1682
1814
|
},
|
|
1683
|
-
list: [{
|
|
1684
|
-
id: agentId,
|
|
1685
|
-
|
|
1686
|
-
|
|
1815
|
+
list: [{
|
|
1816
|
+
id: agentId,
|
|
1817
|
+
workspace: 'workspace',
|
|
1818
|
+
agentDir: `agents/${agentId}/agent`,
|
|
1819
|
+
model: { primary: state.config.model, fallbacks: [] },
|
|
1820
|
+
}],
|
|
1687
1821
|
},
|
|
1688
1822
|
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
|
1689
1823
|
channels: ch.channelConfig,
|
|
1690
1824
|
tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
|
|
1691
|
-
gateway: {
|
|
1692
|
-
port: 18791,
|
|
1693
|
-
mode: 'local',
|
|
1694
|
-
bind: '
|
|
1695
|
-
controlUi: {
|
|
1696
|
-
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
1697
|
-
},
|
|
1698
|
-
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
1825
|
+
gateway: {
|
|
1826
|
+
port: 18791,
|
|
1827
|
+
mode: 'local',
|
|
1828
|
+
bind: 'loopback',
|
|
1829
|
+
controlUi: {
|
|
1830
|
+
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
1831
|
+
},
|
|
1832
|
+
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
1699
1833
|
},
|
|
1700
1834
|
};
|
|
1701
1835
|
|
|
@@ -1793,13 +1927,14 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1793
1927
|
botToken: meta.token || '<your_bot_token>',
|
|
1794
1928
|
ackReaction: '👍',
|
|
1795
1929
|
}]));
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1930
|
+
const nativeOpenClawRoot = '.openclaw';
|
|
1931
|
+
clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
|
|
1932
|
+
id: meta.agentId,
|
|
1933
|
+
name: meta.name,
|
|
1934
|
+
workspace: meta.workspaceDir,
|
|
1935
|
+
agentDir: `agents/${meta.agentId}/agent`,
|
|
1936
|
+
model: { primary: state.config.model, fallbacks: [] },
|
|
1937
|
+
}));
|
|
1803
1938
|
clawConfig.bindings = multiBotAgentMetas.map((meta) => ({
|
|
1804
1939
|
agentId: meta.agentId,
|
|
1805
1940
|
match: { channel: 'telegram', accountId: meta.accountId },
|
|
@@ -1832,23 +1967,27 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1832
1967
|
allow: multiBotAgentMetas.map((meta) => meta.agentId),
|
|
1833
1968
|
},
|
|
1834
1969
|
};
|
|
1835
|
-
clawConfig.plugins = {
|
|
1836
|
-
entries: {
|
|
1837
|
-
'telegram-multibot-relay': { enabled: true },
|
|
1838
|
-
},
|
|
1839
|
-
};
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1970
|
+
clawConfig.plugins = {
|
|
1971
|
+
entries: {
|
|
1972
|
+
'telegram-multibot-relay': { enabled: true },
|
|
1973
|
+
},
|
|
1974
|
+
};
|
|
1975
|
+
if (!state.config.skills.includes('memory')) {
|
|
1976
|
+
clawConfig.plugins.slots = { ...(clawConfig.plugins.slots || {}), memory: 'none' };
|
|
1977
|
+
}
|
|
1978
|
+
} else if (state.config.plugins.length > 0 || !state.config.skills.includes('memory')) {
|
|
1979
|
+
// Non-multibot: write selected visible plugins into openclaw.json
|
|
1980
|
+
const pluginEntries = {};
|
|
1981
|
+
state.config.plugins.forEach((pid) => {
|
|
1982
|
+
const plugin = PLUGINS.find((p) => p.id === pid);
|
|
1983
|
+
if (!plugin || plugin.hidden) return;
|
|
1984
|
+
pluginEntries[plugin.package || pid] = { enabled: true };
|
|
1985
|
+
});
|
|
1986
|
+
clawConfig.plugins = { entries: pluginEntries };
|
|
1987
|
+
if (!state.config.skills.includes('memory')) {
|
|
1988
|
+
clawConfig.plugins.slots = { ...(clawConfig.plugins.slots || {}), memory: 'none' };
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1852
1991
|
|
|
1853
1992
|
setOutput('out-openclaw-json', JSON.stringify(clawConfig, null, 2));
|
|
1854
1993
|
|
|
@@ -1915,7 +2054,7 @@ model:
|
|
|
1915
2054
|
: '';
|
|
1916
2055
|
|
|
1917
2056
|
// Browser Automation: extra Docker deps
|
|
1918
|
-
const browserAptExtra =
|
|
2057
|
+
const browserAptExtra = ' socat';
|
|
1919
2058
|
const browserInstallLines = hasBrowser
|
|
1920
2059
|
? [
|
|
1921
2060
|
'',
|
|
@@ -1935,15 +2074,16 @@ model:
|
|
|
1935
2074
|
const pluginInstallCmd = allPlugins.length > 0
|
|
1936
2075
|
? `openclaw plugins install ${allPlugins.join(' ')} 2>/dev/null || true && ${relayPluginInstallCmd}`
|
|
1937
2076
|
: relayPluginInstallCmd;
|
|
1938
|
-
const gatewayCmd = 'openclaw gateway run';
|
|
1939
|
-
const browserPrefix = hasBrowser
|
|
1940
|
-
? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
|
|
1941
|
-
: '';
|
|
1942
|
-
|
|
1943
|
-
|
|
2077
|
+
const gatewayCmd = 'openclaw gateway run';
|
|
2078
|
+
const browserPrefix = hasBrowser
|
|
2079
|
+
? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
|
|
2080
|
+
: '';
|
|
2081
|
+
const gatewayBridgePrefix = 'socat TCP-LISTEN:18791,fork,reuseaddr TCP:127.0.0.1:18791 & ';
|
|
2082
|
+
// Patch config on every startup to keep gateway settings stable
|
|
2083
|
+
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:'loopback',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});delete c.gateway.customBindHost;fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
|
|
1944
2084
|
// Auto-approve device pairing after gateway starts (required since v2026.3.x)
|
|
1945
2085
|
const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
|
|
1946
|
-
const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${autoApproveCmd}${gatewayCmd}"`;
|
|
2086
|
+
const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${gatewayBridgePrefix}${autoApproveCmd}${gatewayCmd}"`;
|
|
1947
2087
|
|
|
1948
2088
|
const dockerfile = `FROM node:22-slim
|
|
1949
2089
|
|
|
@@ -1951,7 +2091,7 @@ RUN apt-get update && apt-get install -y git curl${browserAptExtra} && rm -rf /v
|
|
|
1951
2091
|
|
|
1952
2092
|
|
|
1953
2093
|
ARG CACHEBUST=${Date.now()}
|
|
1954
|
-
RUN npm install -g openclaw@2026.4.5
|
|
2094
|
+
RUN npm install -g openclaw@2026.4.5 ${openClawRuntimePackages}${skillLines}${browserInstallLines}
|
|
1955
2095
|
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
2096
|
WORKDIR /root/.openclaw
|
|
1957
2097
|
|
|
@@ -1969,8 +2109,8 @@ ${finalCmd}`;
|
|
|
1969
2109
|
|
|
1970
2110
|
// ─── Dynamic Smart Route Sync Script ────────────────────────────────────────
|
|
1971
2111
|
// Background loop inside 9Router container every 30s.
|
|
1972
|
-
//
|
|
1973
|
-
//
|
|
2112
|
+
// Sync against the 9Router API so smart-route matches the current
|
|
2113
|
+
// active provider set instead of stale db-only state.
|
|
1974
2114
|
const syncScript = `const fs=require('fs');const INTERVAL=30000;const p='/root/.9router/db.json';
|
|
1975
2115
|
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
2116
|
console.log('[sync-combo] 9Router sync loop started...');
|
|
@@ -2755,17 +2895,19 @@ fi
|
|
|
2755
2895
|
const otherBotNames = state.bots.slice(0, state.botCount).filter((_, idx) => idx !== i).map((peer, idx) => peer?.name || `Bot ${idx + 1}`);
|
|
2756
2896
|
const botConfig = JSON.parse(JSON.stringify(clawConfig));
|
|
2757
2897
|
botConfig.agents.defaults.model = { primary: state.config.model, fallbacks: [] };
|
|
2758
|
-
botConfig.agents.list = [{
|
|
2759
|
-
id: botAgentId,
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2898
|
+
botConfig.agents.list = [{
|
|
2899
|
+
id: botAgentId,
|
|
2900
|
+
workspace: 'workspace',
|
|
2901
|
+
agentDir: `agents/${botAgentId}/agent`,
|
|
2902
|
+
model: { primary: state.config.model, fallbacks: [] },
|
|
2903
|
+
}];
|
|
2904
|
+
botConfig.gateway = {
|
|
2905
|
+
...(botConfig.gateway || {}),
|
|
2906
|
+
port: 18791,
|
|
2907
|
+
mode: 'local',
|
|
2908
|
+
bind: 'loopback',
|
|
2909
|
+
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
2910
|
+
};
|
|
2769
2911
|
|
|
2770
2912
|
const botAgentYaml = `name: ${botAgentId}
|
|
2771
2913
|
description: "${botDesc}"
|
|
@@ -2967,40 +3109,63 @@ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
|
|
|
2967
3109
|
const ch = CHANNELS[state.channel];
|
|
2968
3110
|
const is9Router = !!(provider && provider.isProxy);
|
|
2969
3111
|
const isOllama = !!(provider && provider.isLocal);
|
|
2970
|
-
const hasBrowser = state.config.skills.includes('browser');
|
|
2971
|
-
const
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
const
|
|
3112
|
+
const hasBrowser = state.config.skills.includes('browser');
|
|
3113
|
+
const nativeSkillConfigs = state.config.skills
|
|
3114
|
+
.map((sid) => SKILLS.find((s) => s.id === sid))
|
|
3115
|
+
.filter((skill) => skill && skill.id !== 'scheduler' && skill.slug && skill.slug !== 'browser-automation');
|
|
3116
|
+
const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
|
|
3117
|
+
const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
|
|
3118
|
+
const projectDir = state.config.projectPath || '.';
|
|
3119
|
+
const todayStamp = new Date().toISOString().slice(0, 10);
|
|
3120
|
+
|
|
3121
|
+
const allPlugins = [];
|
|
2976
3122
|
if (ch && ch.pluginInstall) allPlugins.push(ch.pluginInstall);
|
|
2977
3123
|
state.config.plugins.forEach(function(pid) {
|
|
2978
3124
|
const p = PLUGINS.find((x) => x.id === pid);
|
|
2979
3125
|
if (p) allPlugins.push(p.package);
|
|
2980
3126
|
});
|
|
2981
|
-
if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
|
|
3127
|
+
if (isMultiBot && state.channel === 'telegram') allPlugins.push(relayPluginSpec);
|
|
2982
3128
|
const pluginCmd = allPlugins.length > 0 ? ('call npm exec -- openclaw plugins install ' + allPlugins.join(' ') + ' || goto :fail') : '';
|
|
3129
|
+
const nativeSkillInstallCmds = nativeSkillConfigs.map((skill) => `call openclaw skills install ${skill.slug} || echo Warning: Failed to install skill ${skill.slug}`);
|
|
2983
3130
|
|
|
2984
3131
|
function native9RouterSyncScriptContent() {
|
|
2985
3132
|
return `const fs=require('fs');
|
|
2986
3133
|
const path=require('path');
|
|
2987
3134
|
const INTERVAL=30000;
|
|
2988
|
-
const p=path.join(process.env.
|
|
3135
|
+
const p=path.join(process.env.DATA_DIR||'.9router','db.json');
|
|
3136
|
+
const ROUTER='http://localhost:20128';
|
|
2989
3137
|
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']};
|
|
2990
|
-
|
|
3138
|
+
console.log('[sync-combo] 9Router sync loop started...');
|
|
3139
|
+
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);`;
|
|
2991
3140
|
}
|
|
2992
3141
|
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
3142
|
+
function native9RouterServerEntryLookup() {
|
|
3143
|
+
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);\"";
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
function windowsHiddenNodeLaunch(targetPath, extraEnv = {}) {
|
|
3147
|
+
function quotePowerShellSingle(value) {
|
|
3148
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
3149
|
+
}
|
|
3150
|
+
const envAssignments = Object.entries(extraEnv)
|
|
3151
|
+
.map(([key, value]) => `$env:${key}=${quotePowerShellSingle(String(value))}`)
|
|
3152
|
+
.join('; ');
|
|
3153
|
+
return `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${envAssignments ? `${envAssignments}; ` : ''}Start-Process -WindowStyle Hidden -FilePath (Get-Command node).Source -ArgumentList @('${targetPath.replace(/'/g, "''")}')"`;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// ─── Shared initializer (provider install) ───────────────────────────────
|
|
3157
|
+
function providerLines(arr, shell) {
|
|
3158
|
+
if (is9Router) {
|
|
3159
|
+
if (shell === 'bat') {
|
|
2997
3160
|
arr.push('call npm install -g 9router || goto :fail');
|
|
2998
|
-
arr.push(
|
|
3161
|
+
arr.push(`for /f "usebackq delims=" %%I in (\`${native9RouterServerEntryLookup()}\`) do set "NINE_ROUTER_ENTRY=%%I"`);
|
|
3162
|
+
arr.push(windowsHiddenNodeLaunch('%NINE_ROUTER_ENTRY%', { PORT: '20128', HOSTNAME: '0.0.0.0', DATA_DIR: '%DATA_DIR%' }));
|
|
2999
3163
|
arr.push('timeout /t 5 /nobreak >nul');
|
|
3000
|
-
} else {
|
|
3001
|
-
arr.push('npm install -g 9router');
|
|
3002
|
-
arr.push(
|
|
3003
|
-
arr.push('nohup
|
|
3164
|
+
} else {
|
|
3165
|
+
arr.push('npm install -g 9router');
|
|
3166
|
+
arr.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
|
|
3167
|
+
arr.push('nohup env PORT=20128 HOSTNAME=0.0.0.0 DATA_DIR="$PWD/.9router" node "$NINE_ROUTER_ENTRY" >/tmp/9router.log 2>&1 &');
|
|
3168
|
+
arr.push('nohup env DATA_DIR="$PWD/.9router" node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
|
|
3004
3169
|
arr.push('sleep 3');
|
|
3005
3170
|
}
|
|
3006
3171
|
} else if (isOllama) {
|
|
@@ -3061,7 +3226,7 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3061
3226
|
order: { ollama: ['ollama:default'] },
|
|
3062
3227
|
};
|
|
3063
3228
|
} else {
|
|
3064
|
-
const authProviderName = provider.isProxy ? '9router' : provider
|
|
3229
|
+
const authProviderName = provider.isProxy ? '9router' : state.config.provider;
|
|
3065
3230
|
const authProfileId = provider.isProxy ? '9router-proxy' : `${authProviderName}:default`;
|
|
3066
3231
|
const authKeyValue = provider.isProxy
|
|
3067
3232
|
? 'sk-no-key'
|
|
@@ -3114,13 +3279,13 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3114
3279
|
timeoutSeconds: provider.isLocal ? 900 : 120,
|
|
3115
3280
|
...(provider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
|
|
3116
3281
|
},
|
|
3117
|
-
list: multiBotAgentMetas.map((meta) => ({
|
|
3118
|
-
id: meta.agentId,
|
|
3119
|
-
name: meta.name,
|
|
3120
|
-
workspace:
|
|
3121
|
-
agentDir:
|
|
3122
|
-
model: { primary: state.config.model, fallbacks: [] },
|
|
3123
|
-
})),
|
|
3282
|
+
list: multiBotAgentMetas.map((meta) => ({
|
|
3283
|
+
id: meta.agentId,
|
|
3284
|
+
name: meta.name,
|
|
3285
|
+
workspace: meta.workspaceDir,
|
|
3286
|
+
agentDir: `agents/${meta.agentId}/agent`,
|
|
3287
|
+
model: { primary: state.config.model, fallbacks: [] },
|
|
3288
|
+
})),
|
|
3124
3289
|
},
|
|
3125
3290
|
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
|
3126
3291
|
bindings: multiBotAgentMetas.map((meta) => ({
|
|
@@ -3160,18 +3325,21 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3160
3325
|
'telegram-multibot-relay': { enabled: true },
|
|
3161
3326
|
},
|
|
3162
3327
|
},
|
|
3163
|
-
gateway: {
|
|
3164
|
-
port: 18791,
|
|
3165
|
-
mode: 'local',
|
|
3166
|
-
bind: '
|
|
3167
|
-
controlUi: {
|
|
3168
|
-
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
3169
|
-
},
|
|
3170
|
-
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
3171
|
-
},
|
|
3172
|
-
};
|
|
3173
|
-
|
|
3174
|
-
|
|
3328
|
+
gateway: {
|
|
3329
|
+
port: 18791,
|
|
3330
|
+
mode: 'local',
|
|
3331
|
+
bind: 'loopback',
|
|
3332
|
+
controlUi: {
|
|
3333
|
+
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
3334
|
+
},
|
|
3335
|
+
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
3336
|
+
},
|
|
3337
|
+
};
|
|
3338
|
+
if (!state.config.skills.includes('memory')) {
|
|
3339
|
+
cfg.plugins = { ...(cfg.plugins || {}), slots: { ...((cfg.plugins && cfg.plugins.slots) || {}), memory: 'none' } };
|
|
3340
|
+
}
|
|
3341
|
+
return JSON.stringify(cfg, null, 2);
|
|
3342
|
+
}
|
|
3175
3343
|
|
|
3176
3344
|
function sharedNativeFileMap() {
|
|
3177
3345
|
const files = {
|
|
@@ -3196,8 +3364,8 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3196
3364
|
const toolsMd = isVi
|
|
3197
3365
|
? `# Huong dan su dung Tools\n\n## Skills da cai\n${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(Chua co skill nao)_'}\n\n## Quy uoc\n- Uu tien dung tool thay vi doan\n- Browser: dung khi user yeu cau thao tac web\n- Memory: cap nhat khi biet thong tin quan trong`
|
|
3198
3366
|
: `# Tool Usage Guide\n\n## Installed Skills\n${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills installed)_'}\n\n## Conventions\n- Prefer tools over guessing\n- Use Browser for explicit web tasks\n- Update Memory when important user info appears`;
|
|
3199
|
-
const memoryMd = isVi ? '# Bo nho dai han\n\n## Ghi chu\n- _(Chua co gi)_' : '# Long-term Memory\n\n## Notes\n- _(Nothing yet)_';
|
|
3200
|
-
for (const meta of multiBotAgentMetas) {
|
|
3367
|
+
const memoryMd = isVi ? '# Bo nho dai han\n\n## Ghi chu\n- _(Chua co gi)_' : '# Long-term Memory\n\n## Notes\n- _(Nothing yet)_';
|
|
3368
|
+
for (const meta of multiBotAgentMetas) {
|
|
3201
3369
|
const ownAliases = [meta.name, meta.slashCmd, `bot ${meta.idx + 1}`].filter(Boolean);
|
|
3202
3370
|
const otherBots = multiBotAgentMetas.filter((peer) => peer.agentId !== meta.agentId);
|
|
3203
3371
|
const relayTargetNames = otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`';
|
|
@@ -3219,10 +3387,10 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3219
3387
|
files[`.openclaw/${meta.workspaceDir}/RELAY.md`] = isVi
|
|
3220
3388
|
? `# Telegram Relay Playbook\n\n## Muc tieu\n- Cho phep bot mo loi goi bot dich noi bo, sau do bot dich tra loi cong khai bang chinh account cua minh.\n\n## Protocol\n1. Bot mo loi gui 1 cau ngan xac nhan se hoi bot dich.\n2. Bot mo loi handoff noi bo bang dung agent id trong \`TEAM.md\`.\n3. Bot dich tra loi cong khai trong cung chat/thread hien tai.\n4. Neu thay \`[[reply_to_current]]\` hoac Telegram send/sendMessage action kha dung, uu tien dung de bam dung message goc.\n5. Neu handoff that bai ro rang, chi bot mo loi moi duoc fallback tom tat.\n`
|
|
3221
3389
|
: `# Telegram Relay Playbook\n\n## Goal\n- Let the caller bot consult the target bot internally, then have the target bot publish the real answer with its own Telegram account.\n\n## Protocol\n1. The caller bot sends one short acknowledgement.\n2. The caller bot hands off internally using the exact agent id from \`TEAM.md\`.\n3. The target bot publishes the real answer into the same chat/thread.\n4. If \`[[reply_to_current]]\` or Telegram send/sendMessage is available, prefer it so the answer attaches to the original user turn.\n5. Only the caller bot may summarize as fallback when the handoff clearly fails.\n`;
|
|
3222
|
-
files[`.openclaw/${meta.workspaceDir}/USER.md`] = userMd;
|
|
3223
|
-
files[`.openclaw/${meta.workspaceDir}/TOOLS.md`] = `${toolsMd}\n\n${isVi ? '## Telegram relay\n- Gateway da bat `ackReaction`, `replyToMode:first`, `actions.sendMessage`, va `actions.reactions`.\n- Khi can relay public bang account cua minh sau internal handoff, uu tien dung outbound Telegram action thay vi output mo ho.' : '## Telegram relay\n- The gateway enables `ackReaction`, `replyToMode:first`, `actions.sendMessage`, and `actions.reactions`.\n- When you need to publish a public relay from your own account after an internal handoff, prefer the Telegram outbound action over an ambiguous plain-text answer.'}`;
|
|
3224
|
-
files[`.openclaw/${meta.workspaceDir}/MEMORY.md`] = memoryMd;
|
|
3225
|
-
if (hasBrowser) {
|
|
3390
|
+
files[`.openclaw/${meta.workspaceDir}/USER.md`] = userMd;
|
|
3391
|
+
files[`.openclaw/${meta.workspaceDir}/TOOLS.md`] = `${toolsMd}\n\n${isVi ? '## Telegram relay\n- Gateway da bat `ackReaction`, `replyToMode:first`, `actions.sendMessage`, va `actions.reactions`.\n- Khi can relay public bang account cua minh sau internal handoff, uu tien dung outbound Telegram action thay vi output mo ho.' : '## Telegram relay\n- The gateway enables `ackReaction`, `replyToMode:first`, `actions.sendMessage`, and `actions.reactions`.\n- When you need to publish a public relay from your own account after an internal handoff, prefer the Telegram outbound action over an ambiguous plain-text answer.'}`;
|
|
3392
|
+
files[`.openclaw/${meta.workspaceDir}/MEMORY.md`] = memoryMd;
|
|
3393
|
+
if (hasBrowser) {
|
|
3226
3394
|
files[`.openclaw/${meta.workspaceDir}/browser-tool.js`] = `const { chromium } = require('playwright');\n(async () => {\n const [,, action, param1, param2] = process.argv;\n const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');\n const ctx = browser.contexts()[0] || await browser.newContext();\n const page = ctx.pages()[0] || await ctx.newPage();\n if (action === 'open') await page.goto(param1, { waitUntil: 'domcontentloaded', timeout: 30000 });\n else if (action === 'click') await page.locator(param1).first().click({ timeout: 5000 });\n else if (action === 'fill') await page.locator(param1).first().fill(param2, { timeout: 5000 });\n else if (action === 'press') await page.keyboard.press(param1);\n else console.log(await page.title(), page.url());\n await browser.close();\n})();\n`;
|
|
3227
3395
|
files[`.openclaw/${meta.workspaceDir}/BROWSER.md`] = isVi
|
|
3228
3396
|
? '# Browser Automation\n\nDung `browser-tool.js` de dieu khien Chrome debug tai `http://127.0.0.1:9222`.'
|
|
@@ -3253,44 +3421,123 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3253
3421
|
}
|
|
3254
3422
|
|
|
3255
3423
|
// ─── Per-bot openclaw.json (minimal — shared workspace) ──────────────────
|
|
3256
|
-
function botConfigContent(botIndex) {
|
|
3257
|
-
const bot = state.bots[botIndex] || {};
|
|
3258
|
-
const botName = bot.name || `Bot ${botIndex + 1}`;
|
|
3259
|
-
const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
3260
|
-
const basePort = 18791 + botIndex;
|
|
3261
|
-
const groupId = state.groupId || '';
|
|
3262
|
-
const
|
|
3263
|
-
|
|
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
|
-
|
|
3424
|
+
function botConfigContent(botIndex) {
|
|
3425
|
+
const bot = state.bots[botIndex] || {};
|
|
3426
|
+
const botName = bot.name || `Bot ${botIndex + 1}`;
|
|
3427
|
+
const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
3428
|
+
const basePort = 18791 + botIndex;
|
|
3429
|
+
const groupId = state.groupId || '';
|
|
3430
|
+
const botProvider = PROVIDERS[bot.provider] || provider;
|
|
3431
|
+
const cfg = {
|
|
3432
|
+
meta: { lastTouchedVersion: '2026.3.24' },
|
|
3433
|
+
agents: {
|
|
3434
|
+
defaults: {
|
|
3435
|
+
model: { primary: bot.model || state.config.model },
|
|
3436
|
+
compaction: { mode: 'safeguard' },
|
|
3437
|
+
timeoutSeconds: botProvider.isLocal ? 900 : 120,
|
|
3438
|
+
...(botProvider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
|
|
3439
|
+
},
|
|
3440
|
+
list: [{
|
|
3441
|
+
id: agentId,
|
|
3442
|
+
workspace: 'workspace',
|
|
3443
|
+
agentDir: `agents/${agentId}/agent`,
|
|
3444
|
+
model: { primary: bot.model || state.config.model }
|
|
3445
|
+
}],
|
|
3446
|
+
},
|
|
3447
|
+
...(botProvider.isProxy ? {
|
|
3448
|
+
models: {
|
|
3449
|
+
mode: 'merge',
|
|
3450
|
+
providers: {
|
|
3451
|
+
'9router': {
|
|
3452
|
+
baseUrl: 'http://localhost:20128/v1',
|
|
3453
|
+
apiKey: 'sk-no-key',
|
|
3454
|
+
api: 'openai-completions',
|
|
3455
|
+
models: [
|
|
3456
|
+
{
|
|
3457
|
+
id: 'smart-route',
|
|
3458
|
+
name: 'Smart Proxy (Auto Route)',
|
|
3459
|
+
contextWindow: 200000,
|
|
3460
|
+
maxTokens: 8192,
|
|
3461
|
+
}
|
|
3462
|
+
]
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
} : {}),
|
|
3467
|
+
...(botProvider.isLocal ? {
|
|
3468
|
+
models: {
|
|
3469
|
+
providers: {
|
|
3470
|
+
ollama: {
|
|
3471
|
+
baseUrl: 'http://localhost:11434',
|
|
3472
|
+
apiKey: 'ollama-local',
|
|
3473
|
+
api: 'ollama',
|
|
3474
|
+
models: [
|
|
3475
|
+
{ id: selectedModel, name: selectedModel, contextWindow: 128000, maxTokens: 8192 }
|
|
3476
|
+
]
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
} : {}),
|
|
3481
|
+
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
|
3482
|
+
channels: {},
|
|
3483
|
+
tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
|
|
3484
|
+
gateway: {
|
|
3485
|
+
port: basePort,
|
|
3486
|
+
mode: 'local',
|
|
3487
|
+
bind: 'loopback',
|
|
3488
|
+
controlUi: {
|
|
3489
|
+
allowedOrigins: getGatewayAllowedOrigins(basePort),
|
|
3490
|
+
},
|
|
3491
|
+
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
3492
|
+
},
|
|
3493
|
+
};
|
|
3494
|
+
|
|
3495
|
+
if (hasBrowser) {
|
|
3496
|
+
cfg.browser = { enabled: true };
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
const skillEntries = {};
|
|
3500
|
+
state.config.skills.forEach((sid) => {
|
|
3501
|
+
const skill = SKILLS.find((s) => s.id === sid);
|
|
3502
|
+
if (!skill) return;
|
|
3503
|
+
if (skill.id === 'scheduler' || skill.slug === 'browser-automation' || !skill.slug) return;
|
|
3504
|
+
skillEntries[skill.slug] = { enabled: true };
|
|
3505
|
+
});
|
|
3506
|
+
if (Object.keys(skillEntries).length > 0) {
|
|
3507
|
+
cfg.skills = { entries: skillEntries };
|
|
3508
|
+
}
|
|
3509
|
+
if (!state.config.skills.includes('memory')) {
|
|
3510
|
+
cfg.plugins = { ...(cfg.plugins || {}), slots: { ...((cfg.plugins && cfg.plugins.slots) || {}), memory: 'none' } };
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
if (state.channel === 'telegram') {
|
|
3514
|
+
cfg.channels.telegram = {
|
|
3515
|
+
enabled: true,
|
|
3516
|
+
dmPolicy: 'open',
|
|
3517
|
+
allowFrom: ['*'],
|
|
3518
|
+
};
|
|
3519
|
+
if (isMultiBot) {
|
|
3520
|
+
cfg.channels.telegram.groupPolicy = groupId ? 'allowlist' : 'open';
|
|
3521
|
+
cfg.channels.telegram.groupAllowFrom = ['*'];
|
|
3522
|
+
cfg.channels.telegram.groups = {
|
|
3523
|
+
[groupId || '*']: {
|
|
3524
|
+
enabled: true,
|
|
3525
|
+
requireMention: false,
|
|
3526
|
+
},
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
} else if (state.channel === 'zalo-personal') {
|
|
3530
|
+
cfg.channels.zalouser = {
|
|
3531
|
+
enabled: true,
|
|
3532
|
+
dmPolicy: 'open',
|
|
3533
|
+
autoReply: true,
|
|
3534
|
+
};
|
|
3535
|
+
} else if (state.channel === 'zalo-bot') {
|
|
3536
|
+
cfg.channels.zalo = { enabled: true, provider: 'official_account' };
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
return JSON.stringify(cfg, null, 2);
|
|
3540
|
+
}
|
|
3294
3541
|
|
|
3295
3542
|
function botAuthProfilesContent(botIndex) {
|
|
3296
3543
|
const bot = state.bots[botIndex] || {};
|
|
@@ -3310,7 +3557,7 @@ const sync=()=>{try{let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catc
|
|
|
3310
3557
|
order: { ollama: ['ollama:default'] },
|
|
3311
3558
|
};
|
|
3312
3559
|
} else {
|
|
3313
|
-
const authProviderName = botProvider.isProxy ? '9router' :
|
|
3560
|
+
const authProviderName = botProvider.isProxy ? '9router' : (bot.provider || state.config.provider);
|
|
3314
3561
|
const authProfileId = botProvider.isProxy ? '9router-proxy' : `${authProviderName}:default`;
|
|
3315
3562
|
const authKeyValue = botProvider.isProxy
|
|
3316
3563
|
? 'sk-no-key'
|
|
@@ -3509,15 +3756,15 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3509
3756
|
|
|
3510
3757
|
## Notes
|
|
3511
3758
|
- _(Nothing yet)_`;
|
|
3512
|
-
const files = {
|
|
3513
|
-
'IDENTITY.md': identityMd,
|
|
3514
|
-
'SOUL.md': soulMd,
|
|
3515
|
-
'AGENTS.md': agentsMd + extraAgentsMd,
|
|
3516
|
-
'TEAM.md': teamMd,
|
|
3517
|
-
'USER.md': userMd,
|
|
3518
|
-
'TOOLS.md': toolsMd,
|
|
3519
|
-
'MEMORY.md': memoryMd,
|
|
3520
|
-
};
|
|
3759
|
+
const files = {
|
|
3760
|
+
'IDENTITY.md': identityMd,
|
|
3761
|
+
'SOUL.md': soulMd,
|
|
3762
|
+
'AGENTS.md': agentsMd + extraAgentsMd,
|
|
3763
|
+
'TEAM.md': teamMd,
|
|
3764
|
+
'USER.md': userMd,
|
|
3765
|
+
'TOOLS.md': toolsMd,
|
|
3766
|
+
'MEMORY.md': memoryMd,
|
|
3767
|
+
};
|
|
3521
3768
|
if (hasBrowser) {
|
|
3522
3769
|
files['browser-tool.js'] = `const { chromium } = require('playwright');\n(async () => {\n const [,, action, param1, param2] = process.argv;\n const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');\n const ctx = browser.contexts()[0] || await browser.newContext();\n const page = ctx.pages()[0] || await ctx.newPage();\n if (action === 'open') await page.goto(param1, { waitUntil: 'domcontentloaded', timeout: 30000 });\n else if (action === 'click') await page.locator(param1).first().click({ timeout: 5000 });\n else if (action === 'fill') await page.locator(param1).first().fill(param2, { timeout: 5000 });\n else if (action === 'press') await page.keyboard.press(param1);\n else console.log(await page.title(), page.url());\n await browser.close();\n})();\n`;
|
|
3523
3770
|
files['BROWSER.md'] = isVi
|
|
@@ -3566,18 +3813,29 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3566
3813
|
.replace(/%/g, '%%');
|
|
3567
3814
|
}
|
|
3568
3815
|
|
|
3569
|
-
function appendBatWriteCommands(arr, files) {
|
|
3570
|
-
Object.entries(files).forEach(([relPath, content]) => {
|
|
3571
|
-
const winPath = relPath.replace(/\//g, '\\');
|
|
3572
|
-
const dir = winPath.substring(0, winPath.lastIndexOf('\\'));
|
|
3573
|
-
if (dir) arr.push(`if not exist "${dir}" mkdir "${dir}"`);
|
|
3816
|
+
function appendBatWriteCommands(arr, files) {
|
|
3817
|
+
Object.entries(files).forEach(([relPath, content]) => {
|
|
3818
|
+
const winPath = relPath.replace(/\//g, '\\');
|
|
3819
|
+
const dir = winPath.substring(0, winPath.lastIndexOf('\\'));
|
|
3820
|
+
if (dir) arr.push(`if not exist "${dir}" mkdir "${dir}"`);
|
|
3574
3821
|
arr.push(`> "${winPath}" (`);
|
|
3575
3822
|
content.split('\n').forEach((line) => {
|
|
3576
3823
|
arr.push(line.length ? `echo(${batEscapeEchoLine(line)}` : 'echo(');
|
|
3577
3824
|
});
|
|
3578
|
-
arr.push(')');
|
|
3579
|
-
});
|
|
3580
|
-
}
|
|
3825
|
+
arr.push(')');
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
function mapWindowsNativeFiles(files) {
|
|
3830
|
+
return Object.fromEntries(Object.entries(files).map(([relPath, content]) => {
|
|
3831
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
3832
|
+
if (normalized === '.env') return ['%PROJECT_DIR%\\.env', content];
|
|
3833
|
+
if (normalized.startsWith('.openclaw/')) {
|
|
3834
|
+
return [`%OPENCLAW_HOME%\\${normalized.slice('.openclaw/'.length).replace(/\//g, '\\')}`, content];
|
|
3835
|
+
}
|
|
3836
|
+
return [`%PROJECT_DIR%\\${normalized.replace(/\//g, '\\')}`, content];
|
|
3837
|
+
}));
|
|
3838
|
+
}
|
|
3581
3839
|
|
|
3582
3840
|
let scriptContent = '';
|
|
3583
3841
|
let scriptName = '';
|
|
@@ -3586,30 +3844,68 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3586
3844
|
if (state.nativeOs === 'win') {
|
|
3587
3845
|
const isDocker = state.deployMode === 'docker';
|
|
3588
3846
|
scriptName = isDocker ? 'setup-openclaw-docker-win.bat' : 'setup-openclaw-win.bat';
|
|
3589
|
-
const lines = [
|
|
3847
|
+
const lines = [
|
|
3590
3848
|
'@echo off',
|
|
3591
3849
|
'setlocal EnableExtensions',
|
|
3592
3850
|
'chcp 65001 >nul',
|
|
3593
|
-
`
|
|
3594
|
-
'
|
|
3595
|
-
'
|
|
3851
|
+
`set "PROJECT_DIR=${projectDir.replace(/\//g, '\\')}"`,
|
|
3852
|
+
'if not exist "%PROJECT_DIR%" mkdir "%PROJECT_DIR%"',
|
|
3853
|
+
'cd /d "%PROJECT_DIR%"',
|
|
3854
|
+
'set "OPENCLAW_HOME=%PROJECT_DIR%\\.openclaw"',
|
|
3855
|
+
'set "OPENCLAW_STATE_DIR=%PROJECT_DIR%\\.openclaw"',
|
|
3856
|
+
'set "DATA_DIR=%PROJECT_DIR%\\.9router"',
|
|
3857
|
+
'set "PATH=%APPDATA%\\npm;%PATH%"',
|
|
3858
|
+
`echo === OpenClaw Setup — Windows${isDocker ? ' Docker' : ' Native'} ===`,
|
|
3859
|
+
'echo.',
|
|
3860
|
+
'echo [1/5] Kiem tra Node.js...',
|
|
3596
3861
|
'where node >nul 2>&1 || (echo ERROR: Node.js chua cai! Tai tai: https://nodejs.org && pause && exit /b 1)',
|
|
3597
3862
|
'echo [2/5] Cai OpenClaw CLI...',
|
|
3598
|
-
|
|
3599
|
-
];
|
|
3600
|
-
providerLines(lines, 'bat');
|
|
3601
|
-
if (
|
|
3602
|
-
|
|
3863
|
+
`call npm install -g openclaw@2026.4.5 ${openClawRuntimePackages} || goto :fail`,
|
|
3864
|
+
];
|
|
3865
|
+
providerLines(lines, 'bat');
|
|
3866
|
+
if (hasBrowser) {
|
|
3867
|
+
lines.push('echo Cai Browser Automation runtime...');
|
|
3868
|
+
lines.push('call npm install -g agent-browser playwright || goto :fail');
|
|
3869
|
+
lines.push('call npx playwright install chromium || goto :fail');
|
|
3870
|
+
}
|
|
3871
|
+
if (nativeSkillInstallCmds.length > 0) {
|
|
3872
|
+
lines.push('echo Cai skills...');
|
|
3873
|
+
lines.push(...nativeSkillInstallCmds);
|
|
3874
|
+
}
|
|
3875
|
+
if (pluginCmd) { lines.push('echo Cai plugins...'); lines.push(pluginCmd); }
|
|
3876
|
+
lines.push('if not exist "%OPENCLAW_HOME%" mkdir "%OPENCLAW_HOME%"');
|
|
3877
|
+
lines.push('if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"');
|
|
3878
|
+
|
|
3603
3879
|
if (isMultiBot) {
|
|
3604
3880
|
lines.push('echo [4/5] Tao runtime multi-agent dung chung...');
|
|
3605
|
-
appendBatWriteCommands(lines, sharedNativeFileMap());
|
|
3606
|
-
if (is9Router) lines.push('
|
|
3881
|
+
appendBatWriteCommands(lines, mapWindowsNativeFiles(sharedNativeFileMap()));
|
|
3882
|
+
if (is9Router) lines.push(windowsHiddenNodeLaunch('%OPENCLAW_HOME%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
|
|
3883
|
+
lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
|
|
3884
|
+
lines.push('echo.');
|
|
3885
|
+
lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
|
|
3886
|
+
lines.push('echo Other reachable URLs: http://localhost:18791');
|
|
3887
|
+
lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
|
|
3888
|
+
if (is9Router) {
|
|
3889
|
+
lines.push('echo.');
|
|
3890
|
+
lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
|
|
3891
|
+
lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
|
|
3892
|
+
}
|
|
3607
3893
|
lines.push('echo [5/5] Khoi dong gateway multi-bot...');
|
|
3608
3894
|
lines.push('call openclaw gateway run');
|
|
3609
3895
|
} else {
|
|
3610
3896
|
lines.push('echo [4/5] Tao file cau hinh...');
|
|
3611
|
-
appendBatWriteCommands(lines, botFiles(0));
|
|
3612
|
-
if (is9Router) lines.push('
|
|
3897
|
+
appendBatWriteCommands(lines, mapWindowsNativeFiles(botFiles(0)));
|
|
3898
|
+
if (is9Router) lines.push(windowsHiddenNodeLaunch('%OPENCLAW_HOME%\\9router-smart-route-sync.js', { DATA_DIR: '%DATA_DIR%' }));
|
|
3899
|
+
lines.push('if not exist "%OPENCLAW_HOME%\\openclaw.json" (echo ERROR: Khong tim thay "%OPENCLAW_HOME%\\openclaw.json" && goto :fail)');
|
|
3900
|
+
lines.push('echo.');
|
|
3901
|
+
lines.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
|
|
3902
|
+
lines.push('echo Other reachable URLs: http://localhost:18791');
|
|
3903
|
+
lines.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
|
|
3904
|
+
if (is9Router) {
|
|
3905
|
+
lines.push('echo.');
|
|
3906
|
+
lines.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
|
|
3907
|
+
lines.push('echo Other reachable URLs: http://localhost:20128/dashboard');
|
|
3908
|
+
}
|
|
3613
3909
|
lines.push('echo [5/5] Khoi dong bot...');
|
|
3614
3910
|
lines.push('call openclaw gateway run');
|
|
3615
3911
|
}
|
|
@@ -3652,18 +3948,24 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3652
3948
|
// ── macOS Native mode: same approach as Ubuntu but no PM2, no apt ────────
|
|
3653
3949
|
// Do NOT use 'npm config set prefix' on macOS — breaks Homebrew Node.
|
|
3654
3950
|
// Use export npm_config_prefix per-session + sudo fallback.
|
|
3655
|
-
const sh = [
|
|
3656
|
-
'#!/usr/bin/env bash', 'set -e',
|
|
3657
|
-
'echo "=== OpenClaw Setup \u2014 macOS Native ==="',
|
|
3658
|
-
'command -v node > /dev/null 2>&1 || { echo "ERROR: Node.js chua cai! https://nodejs.org"; exit 1; }',
|
|
3951
|
+
const sh = [
|
|
3952
|
+
'#!/usr/bin/env bash', 'set -e',
|
|
3953
|
+
'echo "=== OpenClaw Setup \u2014 macOS Native ==="',
|
|
3954
|
+
'command -v node > /dev/null 2>&1 || { echo "ERROR: Node.js chua cai! https://nodejs.org"; exit 1; }',
|
|
3659
3955
|
'# User-local npm prefix (Homebrew-safe — no global npmrc mutation)',
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3956
|
+
'mkdir -p "$HOME/.local/bin"',
|
|
3957
|
+
'export npm_config_prefix="$HOME/.local"',
|
|
3958
|
+
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3959
|
+
`PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
|
|
3960
|
+
'mkdir -p "$PROJECT_DIR"',
|
|
3961
|
+
'cd "$PROJECT_DIR"',
|
|
3962
|
+
'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
|
|
3963
|
+
'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
|
|
3964
|
+
'export DATA_DIR="$PROJECT_DIR/.9router"',
|
|
3965
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.zshrc"',
|
|
3966
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
3967
|
+
'# Install openclaw (user-local first, sudo fallback)',
|
|
3968
|
+
`npm install -g openclaw@2026.4.5 ${openClawRuntimePackages} || sudo npm install -g openclaw@2026.4.5 ${openClawRuntimePackages}`,
|
|
3667
3969
|
];
|
|
3668
3970
|
providerLines(sh, 'sh');
|
|
3669
3971
|
if (pluginCmd) sh.push(pluginCmd);
|
|
@@ -3682,20 +3984,26 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3682
3984
|
// ─── VPS/Ubuntu PM2 .SH ──────────────────────────────────────────────────
|
|
3683
3985
|
} else if (state.nativeOs === 'vps') {
|
|
3684
3986
|
scriptName = 'setup-openclaw-vps.sh';
|
|
3685
|
-
const vps = [
|
|
3686
|
-
'#!/usr/bin/env bash', 'set -e',
|
|
3987
|
+
const vps = [
|
|
3988
|
+
'#!/usr/bin/env bash', 'set -e',
|
|
3687
3989
|
`echo "=== OpenClaw Setup — Ubuntu/VPS${isMultiBot ? ` Multi-Bot (${state.botCount} bots)` : ''} ==="`,
|
|
3688
3990
|
'# Auto-install Node.js 20 LTS if missing',
|
|
3689
3991
|
'if ! command -v node > /dev/null 2>&1; then',
|
|
3690
3992
|
' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
|
|
3691
3993
|
' sudo apt-get install -y nodejs',
|
|
3692
3994
|
'fi',
|
|
3693
|
-
'mkdir -p "$HOME/.local/bin"',
|
|
3694
|
-
'npm config set prefix "$HOME/.local"',
|
|
3695
|
-
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3696
|
-
|
|
3697
|
-
'
|
|
3698
|
-
'
|
|
3995
|
+
'mkdir -p "$HOME/.local/bin"',
|
|
3996
|
+
'npm config set prefix "$HOME/.local"',
|
|
3997
|
+
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3998
|
+
`PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
|
|
3999
|
+
'mkdir -p "$PROJECT_DIR"',
|
|
4000
|
+
'cd "$PROJECT_DIR"',
|
|
4001
|
+
'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
|
|
4002
|
+
'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
|
|
4003
|
+
'export DATA_DIR="$PROJECT_DIR/.9router"',
|
|
4004
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
|
|
4005
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
4006
|
+
`npm install -g openclaw@2026.4.5 ${openClawRuntimePackages} pm2@latest`,
|
|
3699
4007
|
];
|
|
3700
4008
|
providerLines(vps, 'sh');
|
|
3701
4009
|
if (pluginCmd) vps.push(pluginCmd);
|
|
@@ -3705,7 +4013,8 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3705
4013
|
appendShWriteCommands(vps, sharedNativeFileMap());
|
|
3706
4014
|
vps.push('echo "--- Starting shared gateway via PM2 ---"');
|
|
3707
4015
|
if (is9Router) {
|
|
3708
|
-
vps.push(
|
|
4016
|
+
vps.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
|
|
4017
|
+
vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$NINE_ROUTER_ENTRY" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
|
|
3709
4018
|
vps.push('pm2 start --name openclaw-multibot-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
|
|
3710
4019
|
}
|
|
3711
4020
|
vps.push('pm2 start --name openclaw-multibot -- sh -c "openclaw gateway run"');
|
|
@@ -3718,7 +4027,8 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3718
4027
|
} else {
|
|
3719
4028
|
appendShWriteCommands(vps, botFiles(0));
|
|
3720
4029
|
if (is9Router) {
|
|
3721
|
-
vps.push(
|
|
4030
|
+
vps.push(`NINE_ROUTER_ENTRY="$(${native9RouterServerEntryLookup()})"`);
|
|
4031
|
+
vps.push('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$NINE_ROUTER_ENTRY" --name openclaw-9router --interpreter "$(command -v node)"');
|
|
3722
4032
|
vps.push('pm2 start --name openclaw-9router-sync -- sh -c "node ./.openclaw/9router-smart-route-sync.js"');
|
|
3723
4033
|
}
|
|
3724
4034
|
vps.push('pm2 start --name openclaw -- sh -c "openclaw gateway run"');
|
|
@@ -3737,12 +4047,18 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3737
4047
|
' curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -',
|
|
3738
4048
|
' sudo apt-get install -y nodejs',
|
|
3739
4049
|
'fi',
|
|
3740
|
-
'mkdir -p "$HOME/.local/bin"',
|
|
3741
|
-
'npm config set prefix "$HOME/.local"',
|
|
3742
|
-
'export PATH="$HOME/.local/bin:$PATH"',
|
|
3743
|
-
|
|
3744
|
-
'
|
|
3745
|
-
'
|
|
4050
|
+
'mkdir -p "$HOME/.local/bin"',
|
|
4051
|
+
'npm config set prefix "$HOME/.local"',
|
|
4052
|
+
'export PATH="$HOME/.local/bin:$PATH"',
|
|
4053
|
+
`PROJECT_DIR="${projectDir.replace(/"/g, '\\"')}"`,
|
|
4054
|
+
'mkdir -p "$PROJECT_DIR"',
|
|
4055
|
+
'cd "$PROJECT_DIR"',
|
|
4056
|
+
'export OPENCLAW_HOME="$PROJECT_DIR/.openclaw"',
|
|
4057
|
+
'export OPENCLAW_STATE_DIR="$PROJECT_DIR/.openclaw"',
|
|
4058
|
+
'export DATA_DIR="$PROJECT_DIR/.9router"',
|
|
4059
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
|
|
4060
|
+
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
4061
|
+
`npm install -g openclaw@2026.4.5 ${openClawRuntimePackages}`,
|
|
3746
4062
|
];
|
|
3747
4063
|
providerLines(lnx, 'sh');
|
|
3748
4064
|
if (pluginCmd) lnx.push(pluginCmd);
|
|
@@ -3801,10 +4117,12 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3801
4117
|
|
|
3802
4118
|
|
|
3803
4119
|
|
|
3804
|
-
window.downloadNativeScript = function() {
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
const
|
|
4120
|
+
window.downloadNativeScript = function() {
|
|
4121
|
+
// Regenerate output first so the downloaded script always matches the latest wizard state.
|
|
4122
|
+
generateOutput();
|
|
4123
|
+
const script = window._nativeScript;
|
|
4124
|
+
if (!script) return;
|
|
4125
|
+
const blob = new Blob([script.content], { type: 'text/plain;charset=utf-8' });
|
|
3808
4126
|
const url = URL.createObjectURL(blob);
|
|
3809
4127
|
const a = document.createElement('a');
|
|
3810
4128
|
a.href = url; a.download = script.name; a.style.display = 'none';
|