create-openclaw-bot 5.1.15 → 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 +181 -161
- package/CHANGELOG.vi.md +275 -255
- package/README.md +12 -11
- package/README.vi.md +12 -11
- package/cli.js +155 -34
- package/package.json +1 -1
- package/setup.js +288 -152
- package/style.css +168 -37
- package/tests/smoke-cli-logic.mjs +41 -11
- 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',
|
|
@@ -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 = {
|
|
@@ -1064,71 +1136,117 @@
|
|
|
1064
1136
|
|
|
1065
1137
|
// Update model dropdown
|
|
1066
1138
|
const modelSelect = document.getElementById('cfg-model');
|
|
1067
|
-
if (modelSelect) {
|
|
1068
|
-
modelSelect.innerHTML = p.models.map((m) =>
|
|
1069
|
-
`<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>`
|
|
1070
|
-
).join('');
|
|
1071
|
-
}
|
|
1072
|
-
|
|
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
|
+
};
|
|
1073
1147
|
|
|
1074
1148
|
function renderPluginGrid() {
|
|
1075
1149
|
const lang = document.getElementById('cfg-language')?.value || 'vi';
|
|
1076
1150
|
|
|
1077
1151
|
// Skills grid (agent capabilities from ClawHub)
|
|
1078
|
-
const skillGrid = document.getElementById('plugin-grid');
|
|
1079
|
-
if (skillGrid) {
|
|
1080
|
-
skillGrid.innerHTML = SKILLS.map((s) => `
|
|
1081
|
-
<label class="plugin-card" data-skill="${s.id}">
|
|
1082
|
-
<input type="checkbox" class="plugin-checkbox" value="${s.id}" onchange="window.__toggleSkill('${s.id}', this.checked)">
|
|
1083
|
-
<div class="plugin-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
+
}
|
|
1093
1188
|
|
|
1094
1189
|
// Plugins grid (npm packages — extra channels/extensions)
|
|
1095
1190
|
// Filter out hidden plugins from user-facing grid
|
|
1096
1191
|
const visiblePlugins = PLUGINS.filter((p) => !p.hidden);
|
|
1097
1192
|
const pluginGrid = document.getElementById('extra-plugin-grid');
|
|
1098
|
-
if (pluginGrid) {
|
|
1099
|
-
pluginGrid.innerHTML = visiblePlugins.map((p) => `
|
|
1100
|
-
<label class="plugin-card" data-plugin="${p.id}">
|
|
1101
|
-
<input type="checkbox" class="plugin-checkbox" value="${p.id}" onchange="window.__togglePlugin('${p.id}', this.checked)">
|
|
1102
|
-
<div class="plugin-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
+
}
|
|
1122
1226
|
|
|
1123
|
-
window.
|
|
1124
|
-
if (checked && !state.config.
|
|
1125
|
-
state.config.
|
|
1126
|
-
} else {
|
|
1127
|
-
state.config.
|
|
1128
|
-
}
|
|
1129
|
-
document.querySelector(`.plugin-card[data-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
+
};
|
|
1132
1250
|
|
|
1133
1251
|
function bindFormEvents() {
|
|
1134
1252
|
// Language change is now handled by __selectLang
|
|
@@ -1547,6 +1665,7 @@
|
|
|
1547
1665
|
const isLocal = provider.isLocal;
|
|
1548
1666
|
const isTelegramMultiBot = state.botCount > 1 && state.channel === 'telegram';
|
|
1549
1667
|
const relayPluginSpec = 'openclaw-telegram-multibot-relay';
|
|
1668
|
+
const openClawRuntimePackages = 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
|
|
1550
1669
|
|
|
1551
1670
|
function buildRelayPluginInstallCommand(prefix) {
|
|
1552
1671
|
return `${prefix} plugins install ${relayPluginSpec} 2>/dev/null || true`;
|
|
@@ -1693,10 +1812,12 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1693
1812
|
timeoutSeconds: isLocal ? 900 : 120,
|
|
1694
1813
|
...(isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
|
|
1695
1814
|
},
|
|
1696
|
-
list: [{
|
|
1697
|
-
id: agentId,
|
|
1698
|
-
|
|
1699
|
-
|
|
1815
|
+
list: [{
|
|
1816
|
+
id: agentId,
|
|
1817
|
+
workspace: 'workspace',
|
|
1818
|
+
agentDir: `agents/${agentId}/agent`,
|
|
1819
|
+
model: { primary: state.config.model, fallbacks: [] },
|
|
1820
|
+
}],
|
|
1700
1821
|
},
|
|
1701
1822
|
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
|
1702
1823
|
channels: ch.channelConfig,
|
|
@@ -1704,8 +1825,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1704
1825
|
gateway: {
|
|
1705
1826
|
port: 18791,
|
|
1706
1827
|
mode: 'local',
|
|
1707
|
-
bind: '
|
|
1708
|
-
customBindHost: '0.0.0.0',
|
|
1828
|
+
bind: 'loopback',
|
|
1709
1829
|
controlUi: {
|
|
1710
1830
|
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
1711
1831
|
},
|
|
@@ -1811,8 +1931,8 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1811
1931
|
clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
|
|
1812
1932
|
id: meta.agentId,
|
|
1813
1933
|
name: meta.name,
|
|
1814
|
-
workspace:
|
|
1815
|
-
agentDir:
|
|
1934
|
+
workspace: meta.workspaceDir,
|
|
1935
|
+
agentDir: `agents/${meta.agentId}/agent`,
|
|
1816
1936
|
model: { primary: state.config.model, fallbacks: [] },
|
|
1817
1937
|
}));
|
|
1818
1938
|
clawConfig.bindings = multiBotAgentMetas.map((meta) => ({
|
|
@@ -1847,23 +1967,27 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
|
|
|
1847
1967
|
allow: multiBotAgentMetas.map((meta) => meta.agentId),
|
|
1848
1968
|
},
|
|
1849
1969
|
};
|
|
1850
|
-
clawConfig.plugins = {
|
|
1851
|
-
entries: {
|
|
1852
|
-
'telegram-multibot-relay': { enabled: true },
|
|
1853
|
-
},
|
|
1854
|
-
};
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
}
|
|
1866
|
-
|
|
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
|
+
}
|
|
1867
1991
|
|
|
1868
1992
|
setOutput('out-openclaw-json', JSON.stringify(clawConfig, null, 2));
|
|
1869
1993
|
|
|
@@ -1930,7 +2054,7 @@ model:
|
|
|
1930
2054
|
: '';
|
|
1931
2055
|
|
|
1932
2056
|
// Browser Automation: extra Docker deps
|
|
1933
|
-
const browserAptExtra =
|
|
2057
|
+
const browserAptExtra = ' socat';
|
|
1934
2058
|
const browserInstallLines = hasBrowser
|
|
1935
2059
|
? [
|
|
1936
2060
|
'',
|
|
@@ -1950,15 +2074,16 @@ model:
|
|
|
1950
2074
|
const pluginInstallCmd = allPlugins.length > 0
|
|
1951
2075
|
? `openclaw plugins install ${allPlugins.join(' ')} 2>/dev/null || true && ${relayPluginInstallCmd}`
|
|
1952
2076
|
: relayPluginInstallCmd;
|
|
1953
|
-
const gatewayCmd = 'openclaw gateway run';
|
|
1954
|
-
const browserPrefix = hasBrowser
|
|
1955
|
-
? 'socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 & '
|
|
1956
|
-
: '';
|
|
1957
|
-
|
|
1958
|
-
|
|
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));}\\" && `;
|
|
1959
2084
|
// Auto-approve device pairing after gateway starts (required since v2026.3.x)
|
|
1960
2085
|
const autoApproveCmd = '(while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done) & ';
|
|
1961
|
-
const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${autoApproveCmd}${gatewayCmd}"`;
|
|
2086
|
+
const finalCmd = `CMD sh -c "${pluginInstallCmd}${patchCmd}${browserPrefix}${gatewayBridgePrefix}${autoApproveCmd}${gatewayCmd}"`;
|
|
1962
2087
|
|
|
1963
2088
|
const dockerfile = `FROM node:22-slim
|
|
1964
2089
|
|
|
@@ -1966,7 +2091,7 @@ RUN apt-get update && apt-get install -y git curl${browserAptExtra} && rm -rf /v
|
|
|
1966
2091
|
|
|
1967
2092
|
|
|
1968
2093
|
ARG CACHEBUST=${Date.now()}
|
|
1969
|
-
RUN npm install -g openclaw@2026.4.5
|
|
2094
|
+
RUN npm install -g openclaw@2026.4.5 ${openClawRuntimePackages}${skillLines}${browserInstallLines}
|
|
1970
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);}"
|
|
1971
2096
|
WORKDIR /root/.openclaw
|
|
1972
2097
|
|
|
@@ -2770,16 +2895,17 @@ fi
|
|
|
2770
2895
|
const otherBotNames = state.bots.slice(0, state.botCount).filter((_, idx) => idx !== i).map((peer, idx) => peer?.name || `Bot ${idx + 1}`);
|
|
2771
2896
|
const botConfig = JSON.parse(JSON.stringify(clawConfig));
|
|
2772
2897
|
botConfig.agents.defaults.model = { primary: state.config.model, fallbacks: [] };
|
|
2773
|
-
botConfig.agents.list = [{
|
|
2774
|
-
id: botAgentId,
|
|
2775
|
-
|
|
2776
|
-
|
|
2898
|
+
botConfig.agents.list = [{
|
|
2899
|
+
id: botAgentId,
|
|
2900
|
+
workspace: 'workspace',
|
|
2901
|
+
agentDir: `agents/${botAgentId}/agent`,
|
|
2902
|
+
model: { primary: state.config.model, fallbacks: [] },
|
|
2903
|
+
}];
|
|
2777
2904
|
botConfig.gateway = {
|
|
2778
2905
|
...(botConfig.gateway || {}),
|
|
2779
2906
|
port: 18791,
|
|
2780
2907
|
mode: 'local',
|
|
2781
|
-
bind: '
|
|
2782
|
-
customBindHost: '0.0.0.0',
|
|
2908
|
+
bind: 'loopback',
|
|
2783
2909
|
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
2784
2910
|
};
|
|
2785
2911
|
|
|
@@ -2988,8 +3114,9 @@ I am **${botName}**. When asked my name, I answer: _"I'm ${botName}"_.`;
|
|
|
2988
3114
|
.map((sid) => SKILLS.find((s) => s.id === sid))
|
|
2989
3115
|
.filter((skill) => skill && skill.id !== 'scheduler' && skill.slug && skill.slug !== 'browser-automation');
|
|
2990
3116
|
const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
|
|
2991
|
-
const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
|
|
2992
|
-
const projectDir = state.config.projectPath || '.';
|
|
3117
|
+
const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
|
|
3118
|
+
const projectDir = state.config.projectPath || '.';
|
|
3119
|
+
const todayStamp = new Date().toISOString().slice(0, 10);
|
|
2993
3120
|
|
|
2994
3121
|
const allPlugins = [];
|
|
2995
3122
|
if (ch && ch.pluginInstall) allPlugins.push(ch.pluginInstall);
|
|
@@ -3152,13 +3279,13 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
|
|
|
3152
3279
|
timeoutSeconds: provider.isLocal ? 900 : 120,
|
|
3153
3280
|
...(provider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
|
|
3154
3281
|
},
|
|
3155
|
-
list: multiBotAgentMetas.map((meta) => ({
|
|
3156
|
-
id: meta.agentId,
|
|
3157
|
-
name: meta.name,
|
|
3158
|
-
workspace:
|
|
3159
|
-
agentDir:
|
|
3160
|
-
model: { primary: state.config.model, fallbacks: [] },
|
|
3161
|
-
})),
|
|
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
|
+
})),
|
|
3162
3289
|
},
|
|
3163
3290
|
commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
|
|
3164
3291
|
bindings: multiBotAgentMetas.map((meta) => ({
|
|
@@ -3201,16 +3328,18 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
|
|
|
3201
3328
|
gateway: {
|
|
3202
3329
|
port: 18791,
|
|
3203
3330
|
mode: 'local',
|
|
3204
|
-
bind: '
|
|
3205
|
-
customBindHost: '0.0.0.0',
|
|
3331
|
+
bind: 'loopback',
|
|
3206
3332
|
controlUi: {
|
|
3207
3333
|
allowedOrigins: getGatewayAllowedOrigins(18791),
|
|
3208
3334
|
},
|
|
3209
3335
|
auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
|
|
3210
|
-
},
|
|
3211
|
-
};
|
|
3212
|
-
|
|
3213
|
-
|
|
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
|
+
}
|
|
3214
3343
|
|
|
3215
3344
|
function sharedNativeFileMap() {
|
|
3216
3345
|
const files = {
|
|
@@ -3235,8 +3364,8 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
|
|
|
3235
3364
|
const toolsMd = isVi
|
|
3236
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`
|
|
3237
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`;
|
|
3238
|
-
const memoryMd = isVi ? '# Bo nho dai han\n\n## Ghi chu\n- _(Chua co gi)_' : '# Long-term Memory\n\n## Notes\n- _(Nothing yet)_';
|
|
3239
|
-
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) {
|
|
3240
3369
|
const ownAliases = [meta.name, meta.slashCmd, `bot ${meta.idx + 1}`].filter(Boolean);
|
|
3241
3370
|
const otherBots = multiBotAgentMetas.filter((peer) => peer.agentId !== meta.agentId);
|
|
3242
3371
|
const relayTargetNames = otherBots.length ? otherBots.map((peer) => `\`${peer.name}\``).join(', ') : '`bot khac`';
|
|
@@ -3258,10 +3387,10 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
|
|
|
3258
3387
|
files[`.openclaw/${meta.workspaceDir}/RELAY.md`] = isVi
|
|
3259
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`
|
|
3260
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`;
|
|
3261
|
-
files[`.openclaw/${meta.workspaceDir}/USER.md`] = userMd;
|
|
3262
|
-
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.'}`;
|
|
3263
|
-
files[`.openclaw/${meta.workspaceDir}/MEMORY.md`] = memoryMd;
|
|
3264
|
-
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) {
|
|
3265
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`;
|
|
3266
3395
|
files[`.openclaw/${meta.workspaceDir}/BROWSER.md`] = isVi
|
|
3267
3396
|
? '# Browser Automation\n\nDung `browser-tool.js` de dieu khien Chrome debug tai `http://127.0.0.1:9222`.'
|
|
@@ -3308,7 +3437,12 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
|
|
|
3308
3437
|
timeoutSeconds: botProvider.isLocal ? 900 : 120,
|
|
3309
3438
|
...(botProvider.isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
|
|
3310
3439
|
},
|
|
3311
|
-
list: [{
|
|
3440
|
+
list: [{
|
|
3441
|
+
id: agentId,
|
|
3442
|
+
workspace: 'workspace',
|
|
3443
|
+
agentDir: `agents/${agentId}/agent`,
|
|
3444
|
+
model: { primary: bot.model || state.config.model }
|
|
3445
|
+
}],
|
|
3312
3446
|
},
|
|
3313
3447
|
...(botProvider.isProxy ? {
|
|
3314
3448
|
models: {
|
|
@@ -3350,8 +3484,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
|
|
|
3350
3484
|
gateway: {
|
|
3351
3485
|
port: basePort,
|
|
3352
3486
|
mode: 'local',
|
|
3353
|
-
bind: '
|
|
3354
|
-
customBindHost: '0.0.0.0',
|
|
3487
|
+
bind: 'loopback',
|
|
3355
3488
|
controlUi: {
|
|
3356
3489
|
allowedOrigins: getGatewayAllowedOrigins(basePort),
|
|
3357
3490
|
},
|
|
@@ -3373,6 +3506,9 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
|
|
|
3373
3506
|
if (Object.keys(skillEntries).length > 0) {
|
|
3374
3507
|
cfg.skills = { entries: skillEntries };
|
|
3375
3508
|
}
|
|
3509
|
+
if (!state.config.skills.includes('memory')) {
|
|
3510
|
+
cfg.plugins = { ...(cfg.plugins || {}), slots: { ...((cfg.plugins && cfg.plugins.slots) || {}), memory: 'none' } };
|
|
3511
|
+
}
|
|
3376
3512
|
|
|
3377
3513
|
if (state.channel === 'telegram') {
|
|
3378
3514
|
cfg.channels.telegram = {
|
|
@@ -3620,15 +3756,15 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3620
3756
|
|
|
3621
3757
|
## Notes
|
|
3622
3758
|
- _(Nothing yet)_`;
|
|
3623
|
-
const files = {
|
|
3624
|
-
'IDENTITY.md': identityMd,
|
|
3625
|
-
'SOUL.md': soulMd,
|
|
3626
|
-
'AGENTS.md': agentsMd + extraAgentsMd,
|
|
3627
|
-
'TEAM.md': teamMd,
|
|
3628
|
-
'USER.md': userMd,
|
|
3629
|
-
'TOOLS.md': toolsMd,
|
|
3630
|
-
'MEMORY.md': memoryMd,
|
|
3631
|
-
};
|
|
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
|
+
};
|
|
3632
3768
|
if (hasBrowser) {
|
|
3633
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`;
|
|
3634
3770
|
files['BROWSER.md'] = isVi
|
|
@@ -3724,7 +3860,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3724
3860
|
'echo [1/5] Kiem tra Node.js...',
|
|
3725
3861
|
'where node >nul 2>&1 || (echo ERROR: Node.js chua cai! Tai tai: https://nodejs.org && pause && exit /b 1)',
|
|
3726
3862
|
'echo [2/5] Cai OpenClaw CLI...',
|
|
3727
|
-
|
|
3863
|
+
`call npm install -g openclaw@2026.4.5 ${openClawRuntimePackages} || goto :fail`,
|
|
3728
3864
|
];
|
|
3729
3865
|
providerLines(lines, 'bat');
|
|
3730
3866
|
if (hasBrowser) {
|
|
@@ -3829,7 +3965,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3829
3965
|
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.zshrc"',
|
|
3830
3966
|
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
3831
3967
|
'# Install openclaw (user-local first, sudo fallback)',
|
|
3832
|
-
|
|
3968
|
+
`npm install -g openclaw@2026.4.5 ${openClawRuntimePackages} || sudo npm install -g openclaw@2026.4.5 ${openClawRuntimePackages}`,
|
|
3833
3969
|
];
|
|
3834
3970
|
providerLines(sh, 'sh');
|
|
3835
3971
|
if (pluginCmd) sh.push(pluginCmd);
|
|
@@ -3867,7 +4003,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3867
4003
|
'export DATA_DIR="$PROJECT_DIR/.9router"',
|
|
3868
4004
|
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
|
|
3869
4005
|
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
3870
|
-
|
|
4006
|
+
`npm install -g openclaw@2026.4.5 ${openClawRuntimePackages} pm2@latest`,
|
|
3871
4007
|
];
|
|
3872
4008
|
providerLines(vps, 'sh');
|
|
3873
4009
|
if (pluginCmd) vps.push(pluginCmd);
|
|
@@ -3922,7 +4058,7 @@ ${selectedSkillNames.length ? selectedSkillNames.join('\n') : '- _(No skills ins
|
|
|
3922
4058
|
'export DATA_DIR="$PROJECT_DIR/.9router"',
|
|
3923
4059
|
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
|
|
3924
4060
|
'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
|
|
3925
|
-
|
|
4061
|
+
`npm install -g openclaw@2026.4.5 ${openClawRuntimePackages}`,
|
|
3926
4062
|
];
|
|
3927
4063
|
providerLines(lnx, 'sh');
|
|
3928
4064
|
if (pluginCmd) lnx.push(pluginCmd);
|