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/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, '&amp;')
341
+ .replace(/</g, '&lt;')
342
+ .replace(/>/g, '&gt;')
343
+ .replace(/"/g, '&quot;')
344
+ .replace(/'/g, '&#39;');
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
- // Highlight card
1058
- document.querySelectorAll('.provider-card').forEach((c) => c.classList.remove('provider-card--selected'));
1059
- document.querySelector(`.provider-card[data-provider="${key}"]`)?.classList.add('provider-card--selected');
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-card__icon">${s.icon}</div>
1080
- <div class="plugin-card__info">
1081
- <div class="plugin-card__name">${s.name}</div>
1082
- <div class="plugin-card__desc">${lang === 'vi' ? (s.descVi || s.desc) : (s.descEn || s.desc)}</div>
1083
- ${(s.noteVi || s.note) ? `<div class="plugin-card__note">⚙️ ${lang === 'vi' ? (s.noteVi || s.note) : (s.noteEn || s.note)}</div>` : ''}
1084
- </div>
1085
- <div class="plugin-card__check">✓</div>
1086
- </label>
1087
- `).join('');
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-card__icon">${p.icon}</div>
1099
- <div class="plugin-card__info">
1100
- <div class="plugin-card__name">${p.name}</div>
1101
- <div class="plugin-card__desc">${lang === 'vi' ? (p.descVi || p.desc) : (p.descEn || p.desc)}</div>
1102
- </div>
1103
- <div class="plugin-card__check">✓</div>
1104
- </label>
1105
- `).join('');
1106
- }
1107
- }
1108
-
1109
- window.__toggleSkill = function (id, checked) {
1110
- if (checked && !state.config.skills.includes(id)) {
1111
- state.config.skills.push(id);
1112
- } else {
1113
- state.config.skills = state.config.skills.filter((s) => s !== id);
1114
- }
1115
- document.querySelector(`.plugin-card[data-skill="${id}"]`)
1116
- ?.classList.toggle('plugin-card--selected', checked);
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.__togglePlugin = function (id, checked) {
1120
- if (checked && !state.config.plugins.includes(id)) {
1121
- state.config.plugins.push(id);
1122
- } else {
1123
- state.config.plugins = state.config.plugins.filter((p) => p !== id);
1124
- }
1125
- document.querySelector(`.plugin-card[data-plugin="${id}"]`)
1126
- ?.classList.toggle('plugin-card--selected', checked);
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
- // Also save multi-bot tokens individually
1276
- if (state.botCount > 1) {
1277
- for (let i = 0; i < state.botCount; i++) {
1278
- const el = document.getElementById(`key-bot-token-${i}`);
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 = 'clawhub:openclaw-telegram-multibot-relay';
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
- model: { primary: state.config.model, fallbacks: [] },
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: '0.0.0.0',
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
- clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
1797
- id: meta.agentId,
1798
- name: meta.name,
1799
- workspace: `/root/.openclaw/${meta.workspaceDir}`,
1800
- agentDir: `/root/.openclaw/agents/${meta.agentId}/agent`,
1801
- model: { primary: state.config.model, fallbacks: [] },
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
- } else if (state.config.plugins.length > 0) {
1841
- // Non-multibot: write selected visible plugins into openclaw.json
1842
- const pluginEntries = {};
1843
- state.config.plugins.forEach((pid) => {
1844
- const plugin = PLUGINS.find((p) => p.id === pid);
1845
- if (!plugin || plugin.hidden) return;
1846
- pluginEntries[plugin.package || pid] = { enabled: true };
1847
- });
1848
- if (Object.keys(pluginEntries).length > 0) {
1849
- clawConfig.plugins = { entries: pluginEntries };
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 = hasBrowser ? ' socat' : '';
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
- // Patch config on every startup to keep gateway settings stable
1943
- const patchCmd = `node -e \\"const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
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 grammy${skillLines}${browserInstallLines}
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
- // Read providerConnections directly from db.json so smart-route survives
1973
- // dashboard auth/response changes in newer 9Router builds.
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
- model: { primary: state.config.model, fallbacks: [] },
2761
- }];
2762
- botConfig.gateway = {
2763
- ...(botConfig.gateway || {}),
2764
- port: 18791,
2765
- mode: 'local',
2766
- bind: '0.0.0.0',
2767
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
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 selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
2972
- const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
2973
- const projectDir = state.config.projectPath || '.';
2974
-
2975
- const allPlugins = [];
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.HOME||process.env.USERPROFILE||'.','.9router','db.json');
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
- const sync=()=>{try{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));}};const a=(db.providerConnections||[]).filter(c=>c&&c.provider&&c.isActive!==false&&!c.disabled).map(c=>c.provider);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));}}else{db.combos.push(c);fs.writeFileSync(p,JSON.stringify(db,null,2));}}catch{}};sync();setInterval(sync,INTERVAL);`;
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
- // ─── Shared initializer (provider install) ───────────────────────────────
2994
- function providerLines(arr, shell) {
2995
- if (is9Router) {
2996
- if (shell === 'bat') {
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('start "9Router" cmd /k "9router -n -l -H 0.0.0.0 -p 20128 --skip-update"');
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('nohup env PORT=20128 HOSTNAME=0.0.0.0 node "$(npm root -g)/9router/app/server.js" >/tmp/9router.log 2>&1 &');
3003
- arr.push('nohup node ./.openclaw/9router-smart-route-sync.js >/tmp/9router-sync.log 2>&1 &');
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.id;
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: `./.openclaw/${meta.workspaceDir}`,
3121
- agentDir: `./.openclaw/agents/${meta.agentId}/agent`,
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: '0.0.0.0',
3167
- controlUi: {
3168
- allowedOrigins: getGatewayAllowedOrigins(18791),
3169
- },
3170
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3171
- },
3172
- };
3173
- return JSON.stringify(cfg, null, 2);
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 channelConfig = JSON.parse(JSON.stringify(ch.channelConfig || {}));
3263
- if (state.channel === 'telegram' && isMultiBot) {
3264
- channelConfig.groupPolicy = groupId ? 'allowlist' : 'open';
3265
- channelConfig.groupAllowFrom = ['*'];
3266
- channelConfig.groups = {
3267
- [groupId || '*']: {
3268
- enabled: true,
3269
- requireMention: false,
3270
- },
3271
- };
3272
- }
3273
- const cfg = {
3274
- meta: { lastTouchedVersion: '2026.3.24' },
3275
- agents: {
3276
- defaults: { model: { primary: bot.model || state.config.model }, compaction: { mode: 'safeguard' }, timeoutSeconds: 120 },
3277
- list: [{ id: agentId, model: { primary: bot.model || state.config.model } }],
3278
- },
3279
- commands: { native: 'auto', nativeSkills: 'auto', restart: true },
3280
- channels: channelConfig,
3281
- gateway: {
3282
- port: basePort,
3283
- mode: 'local',
3284
- bind: '0.0.0.0',
3285
- controlUi: {
3286
- allowedOrigins: getGatewayAllowedOrigins(basePort),
3287
- },
3288
- auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3289
- },
3290
-
3291
- };
3292
- return JSON.stringify(cfg, null, 2);
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' : botProvider.id;
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
- `echo === OpenClaw Setup — Windows${isDocker ? ' Docker' : ' Native'} ===`,
3594
- 'echo.',
3595
- 'echo [1/5] Kiem tra Node.js...',
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
- 'call npm install -g openclaw@2026.4.5 || goto :fail',
3599
- ];
3600
- providerLines(lines, 'bat');
3601
- if (pluginCmd) { lines.push('echo Cai plugins...'); lines.push(pluginCmd); }
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('start "9Router Smart Route Sync" cmd /k "node .\\.openclaw\\9router-smart-route-sync.js"');
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('start "9Router Smart Route Sync" cmd /k "node .\\.openclaw\\9router-smart-route-sync.js"');
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
- 'mkdir -p "$HOME/.local/bin"',
3661
- 'export npm_config_prefix="$HOME/.local"',
3662
- 'export PATH="$HOME/.local/bin:$PATH"',
3663
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.zshrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.zshrc"',
3664
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3665
- '# Install openclaw (user-local first, sudo fallback)',
3666
- 'npm install -g openclaw@2026.4.5 || sudo npm install -g openclaw@2026.4.5',
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
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
3697
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3698
- 'npm install -g openclaw@2026.4.5 pm2@latest',
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('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-multibot-9router --interpreter "$(command -v node)"');
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('PORT=20128 HOSTNAME=0.0.0.0 pm2 start "$(npm root -g)/9router/app/server.js" --name openclaw-9router --interpreter "$(command -v node)"');
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
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.bashrc" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.bashrc"',
3744
- 'grep -Fqx \'export PATH="$HOME/.local/bin:$PATH"\' "$HOME/.profile" 2>/dev/null || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> "$HOME/.profile"',
3745
- 'npm install -g openclaw@2026.4.5',
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
- const script = window._nativeScript;
3806
- if (!script) return;
3807
- const blob = new Blob([script.content], { type: 'text/plain;charset=utf-8' });
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';