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/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, '&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 = {
@@ -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-card__icon">${s.icon}</div>
1084
- <div class="plugin-card__info">
1085
- <div class="plugin-card__name">${s.name}</div>
1086
- <div class="plugin-card__desc">${lang === 'vi' ? (s.descVi || s.desc) : (s.descEn || s.desc)}</div>
1087
- ${(s.noteVi || s.note) ? `<div class="plugin-card__note">⚙️ ${lang === 'vi' ? (s.noteVi || s.note) : (s.noteEn || s.note)}</div>` : ''}
1088
- </div>
1089
- <div class="plugin-card__check">✓</div>
1090
- </label>
1091
- `).join('');
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-card__icon">${p.icon}</div>
1103
- <div class="plugin-card__info">
1104
- <div class="plugin-card__name">${p.name}</div>
1105
- <div class="plugin-card__desc">${lang === 'vi' ? (p.descVi || p.desc) : (p.descEn || p.desc)}</div>
1106
- </div>
1107
- <div class="plugin-card__check">✓</div>
1108
- </label>
1109
- `).join('');
1110
- }
1111
- }
1112
-
1113
- window.__toggleSkill = function (id, checked) {
1114
- if (checked && !state.config.skills.includes(id)) {
1115
- state.config.skills.push(id);
1116
- } else {
1117
- state.config.skills = state.config.skills.filter((s) => s !== id);
1118
- }
1119
- document.querySelector(`.plugin-card[data-skill="${id}"]`)
1120
- ?.classList.toggle('plugin-card--selected', checked);
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.__togglePlugin = function (id, checked) {
1124
- if (checked && !state.config.plugins.includes(id)) {
1125
- state.config.plugins.push(id);
1126
- } else {
1127
- state.config.plugins = state.config.plugins.filter((p) => p !== id);
1128
- }
1129
- document.querySelector(`.plugin-card[data-plugin="${id}"]`)
1130
- ?.classList.toggle('plugin-card--selected', checked);
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
- model: { primary: state.config.model, fallbacks: [] },
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: 'custom',
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: `${nativeOpenClawRoot}/${meta.workspaceDir}`,
1815
- agentDir: `${nativeOpenClawRoot}/agents/${meta.agentId}/agent`,
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
- } else if (state.config.plugins.length > 0) {
1856
- // Non-multibot: write selected visible plugins into openclaw.json
1857
- const pluginEntries = {};
1858
- state.config.plugins.forEach((pid) => {
1859
- const plugin = PLUGINS.find((p) => p.id === pid);
1860
- if (!plugin || plugin.hidden) return;
1861
- pluginEntries[plugin.package || pid] = { enabled: true };
1862
- });
1863
- if (Object.keys(pluginEntries).length > 0) {
1864
- clawConfig.plugins = { entries: pluginEntries };
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 = hasBrowser ? ' socat' : '';
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
- // Patch config on every startup to keep gateway settings stable
1958
- const patchCmd = `node -e \\"const fs=require('fs'),os=require('os'),p='/root/.openclaw/openclaw.json';if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\" && `;
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 grammy${skillLines}${browserInstallLines}
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
- model: { primary: state.config.model, fallbacks: [] },
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: 'custom',
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: `${nativeProjectOpenClawRoot}/${meta.workspaceDir}`,
3159
- agentDir: `${nativeProjectOpenClawRoot}/agents/${meta.agentId}/agent`,
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: 'custom',
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
- return JSON.stringify(cfg, null, 2);
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: [{ id: agentId, model: { primary: bot.model || state.config.model } }],
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: 'custom',
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
- 'call npm install -g openclaw@2026.4.5 || goto :fail',
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
- 'npm install -g openclaw@2026.4.5 || sudo npm install -g openclaw@2026.4.5',
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
- 'npm install -g openclaw@2026.4.5 pm2@latest',
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
- 'npm install -g openclaw@2026.4.5',
4061
+ `npm install -g openclaw@2026.4.5 ${openClawRuntimePackages}`,
3926
4062
  ];
3927
4063
  providerLines(lnx, 'sh');
3928
4064
  if (pluginCmd) lnx.push(pluginCmd);