create-openclaw-bot 5.8.23 → 5.9.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.
@@ -1,511 +1,528 @@
1
- // @ts-nocheck
2
- /**
3
- * @fileoverview Centralized bot configuration builders — single source of truth.
4
- *
5
- * Generates openclaw.json, auth-profiles.json, exec-approvals.json, and .env content.
6
- * Used by BOTH the Wizard (IIFE bundle) and CLI (CJS require).
7
- *
8
- * Pattern: same as common-gen.js / workspace-gen.js — IIFE + CJS dual export.
9
- */
10
- (function (root) {
11
-
12
- const _common = (typeof root !== 'undefined' && root.__openclawCommon) || {};
13
-
14
- // ── Helper: slugify a bot name into a safe agent ID ─────────────────────────
15
- function slugify(name) {
16
- return String(name || 'bot').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'bot';
17
- }
18
-
19
- // ── Helper: detect if channel is zalo personal ───────────────────────────────
20
- function isZaloPersonal(channelKey) {
21
- return channelKey === 'zalo-personal';
22
- }
23
-
24
- // ── Helper: generate a random token (works in both browser + Node) ──────────
25
- function generateToken() {
26
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
27
- return crypto.randomUUID().replace(/-/g, '');
28
- }
29
- // Fallback for older Node.js
30
- const hex = '0123456789abcdef';
31
- let result = '';
32
- for (let i = 0; i < 32; i++) result += hex[Math.floor(Math.random() * 16)];
33
- return result;
34
- }
35
-
36
-
37
- // ═══════════════════════════════════════════════════════════════════════════════
38
- // buildOpenclawJson — the ONE function that generates the full openclaw.json
39
- // ═══════════════════════════════════════════════════════════════════════════════
40
- /**
41
- * @param {object} opts
42
- * @param {string} opts.channelKey - 'telegram' | 'zalo-personal' | 'zalo-bot'
43
- * @param {string} opts.deployMode - 'docker' | 'native'
44
- * @param {string} opts.providerKey - '9router' | 'openai' | 'ollama' | ...
45
- * @param {object} opts.provider - Provider metadata object from PROVIDERS
46
- * @param {string} opts.model - Primary model ID (e.g. 'smart-route', 'gemma4:e2b')
47
- * @param {boolean} opts.isMultiBot - Multi-bot mode
48
- * @param {Array} opts.agentMetas - [{ agentId, name, desc, persona, token, slashCmd, accountId, workspaceDir }]
49
- * @param {string} opts.groupId - Telegram group ID (multi-bot only)
50
- * @param {Array} opts.selectedSkills - ['browser', 'memory', 'scheduler', ...]
51
- * @param {Array} opts.skills - Full SKILLS registry array
52
- * @param {boolean} opts.hasBrowserDesktop - Browser desktop mode
53
- * @param {boolean} opts.hasBrowserServer - Browser server mode
54
- * @param {number} [opts.gatewayPort=18789]
55
- * @param {Array} [opts.gatewayAllowedOrigins]
56
- * @param {string} [opts.osChoice] - 'windows' | 'macos' | 'vps' | 'ubuntu'
57
- * @param {string} [opts.selectedModel] - For Ollama: specific model selected
58
- */
59
- function buildOpenclawJson(opts) {
60
- const {
61
- channelKey = 'telegram',
62
- deployMode = 'docker',
63
- providerKey = '9router',
64
- provider = {},
65
- model = 'smart-route',
66
- isMultiBot = false,
67
- agentMetas = [],
68
- groupId = '',
69
- selectedSkills = [],
70
- skills = [],
71
- hasBrowserDesktop = false,
72
- hasBrowserServer = false,
73
- gatewayPort = 18789,
74
- gatewayAllowedOrigins = [],
75
- osChoice = '',
76
- selectedModel = '',
77
- routerPort,
78
- } = opts;
79
-
80
- const common = _common;
81
- const is9Router = providerKey === '9router';
82
- const isLocal = !!provider.isLocal;
83
-
84
- // ── agents ────────────────────────────────────────────────────────────────
85
- const agentsList = agentMetas.map((meta) => ({
86
- id: meta.agentId,
87
- ...(meta.name ? { name: meta.name } : {}),
88
- workspace: `/home/node/project/.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
89
- agentDir: `agents/${meta.agentId}/agent`,
90
- model: { primary: model, fallbacks: [] },
91
- }));
92
-
93
- const cfg = {
94
- meta: {
95
- lastTouchedVersion: (_common.OPENCLAW_NPM_SPEC || 'latest').replace('openclaw@', ''),
96
- osChoice,
97
- deployMode,
98
- },
99
- agents: {
100
- defaults: {
101
- model: { primary: model, fallbacks: [] },
102
- compaction: { mode: 'safeguard' },
103
- timeoutSeconds: isLocal ? 900 : 120,
104
- ...(isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
105
- },
106
- list: agentsList,
107
- },
108
- };
109
-
110
- // ── models.providers ──────────────────────────────────────────────────────
111
- if (is9Router && common.build9RouterProviderConfig) {
112
- cfg.models = {
113
- mode: 'merge',
114
- providers: {
115
- '9router': common.build9RouterProviderConfig(
116
- common.get9RouterBaseUrl ? common.get9RouterBaseUrl(deployMode, routerPort) : `http://9router:${routerPort || 20128}/v1`
117
- ),
118
- },
119
- };
120
- } else if (isLocal) {
121
- const ollamaBaseUrl = deployMode === 'docker' ? 'http://ollama:11434' : 'http://localhost:11434';
122
- const OLLAMA_MODELS = (typeof root !== 'undefined' && root.__openclawData && root.__openclawData.OLLAMA_MODELS)
123
- || (typeof _OLLAMA_MODELS !== 'undefined' ? _OLLAMA_MODELS : []);
124
- const modelList = selectedModel
125
- ? [{ id: selectedModel, name: selectedModel, contextWindow: 128000, maxTokens: 8192 }]
126
- : OLLAMA_MODELS;
127
- cfg.models = {
128
- mode: 'merge',
129
- providers: {
130
- ollama: {
131
- baseUrl: ollamaBaseUrl,
132
- api: 'ollama',
133
- apiKey: 'ollama-local',
134
- models: modelList,
135
- },
136
- },
137
- };
138
- }
139
-
140
- // ── commands ──────────────────────────────────────────────────────────────
141
- cfg.commands = { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' };
142
- if (selectedSkills.includes('scheduler')) {
143
- cfg.commands.ownerAllowFrom = ['*'];
144
- }
145
-
146
- // ── bindings (multi-bot or Zalo) ─────────────────────────────────────────
147
- if (agentMetas.length > 0 && isMultiBot && channelKey === 'telegram') {
148
- cfg.bindings = agentMetas.map((meta) => ({
149
- agentId: meta.agentId,
150
- match: { channel: 'telegram', accountId: meta.accountId || 'default' },
151
- }));
152
- } else {
153
- cfg.bindings = [];
154
- }
155
-
156
- // ── channels ─────────────────────────────────────────────────────────────
157
- if (agentMetas.length > 0) {
158
- cfg.channels = buildChannelConfig({
159
- channelKey, isMultiBot, groupId, agentMetas, botName: agentMetas[0]?.name || 'Bot',
160
- agentId: agentMetas[0]?.agentId || 'bot',
161
- });
162
- } else {
163
- cfg.channels = {};
164
- }
165
-
166
- // ── tools ────────────────────────────────────────────────────────────────
167
- cfg.tools = { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
168
- const alsoAllow = [];
169
- if (selectedSkills.includes('scheduler') || selectedSkills.includes('cron')) {
170
- alsoAllow.push('group:automation');
171
- }
172
- if (
173
- selectedSkills.includes('web-search') ||
174
- selectedSkills.includes('web_search') ||
175
- selectedSkills.includes('browser') ||
176
- selectedSkills.includes('browser-automation') ||
177
- hasBrowserDesktop ||
178
- hasBrowserServer
179
- ) {
180
- alsoAllow.push('group:web');
181
- }
182
- if (alsoAllow.length > 0) {
183
- cfg.tools.alsoAllow = alsoAllow;
184
- }
185
- // DuckDuckGo is the bundled, credential-free web_search provider. Auto-detect only
186
- // picks providers that have credentials, so a free provider must be selected
187
- // explicitly, otherwise web_search reports "no provider is available".
188
- if (selectedSkills.includes('web-search') || selectedSkills.includes('web_search')) {
189
- cfg.tools.web = {
190
- ...(cfg.tools.web || {}),
191
- search: { ...((cfg.tools.web && cfg.tools.web.search) || {}), provider: 'duckduckgo' },
192
- };
193
- }
194
- if (isMultiBot) {
195
- cfg.tools.agentToAgent = {
196
- enabled: true,
197
- allow: agentMetas.map((meta) => meta.agentId),
198
- };
199
- }
200
-
201
- // ── gateway ──────────────────────────────────────────────────────────────
202
- cfg.gateway = {
203
- port: gatewayPort,
204
- mode: 'local',
205
- bind: (deployMode === 'docker' || osChoice === 'vps') ? 'custom' : 'loopback',
206
- ...(deployMode === 'docker' || osChoice === 'vps' ? { customBindHost: '0.0.0.0' } : {}),
207
- controlUi: {
208
- allowedOrigins: gatewayAllowedOrigins.length > 0
209
- ? gatewayAllowedOrigins
210
- : [`http://localhost:${gatewayPort}`, `http://127.0.0.1:${gatewayPort}`, `http://0.0.0.0:${gatewayPort}`],
211
- },
212
- auth: { mode: 'token', token: generateToken() },
213
- };
214
-
215
- // ── browser (delegated to browser-automation plugin) ────────────────────
216
-
217
- // ── skills ───────────────────────────────────────────────────────────────
218
- const skillEntries = buildSkillsEntries(skills, selectedSkills);
219
- if (Object.keys(skillEntries).length > 0) {
220
- cfg.skills = { entries: skillEntries };
221
- }
222
-
223
- // ── plugins (memory-core dreaming + openclaw-zalo-mod) ────────────────────────────
224
- const pluginsConfig = buildPluginsConfig({
225
- channelKey,
226
- selectedSkills,
227
- botName: agentMetas[0]?.name || 'Bot',
228
- agentId: agentMetas[0]?.agentId || 'bot',
229
- hasBrowser: hasBrowserDesktop || hasBrowserServer
230
- || selectedSkills.includes('browser') || selectedSkills.includes('browser-automation'),
231
- });
232
- cfg.plugins = pluginsConfig.plugins;
233
-
234
- // ── bindings for zalouser ────────────────────────────────────────────────
235
- if (agentMetas.length > 0 && isZaloPersonal(channelKey)) {
236
- cfg.bindings = cfg.bindings || [];
237
- const firstAgentId = agentMetas[0]?.agentId || 'bot';
238
- if (!cfg.bindings.some(b => b.match && b.match.channel === 'zalouser')) {
239
- cfg.bindings.push({ agentId: firstAgentId, match: { channel: 'zalouser' } });
240
- }
241
- }
242
-
243
- return cfg;
244
- }
245
-
246
-
247
- // ═══════════════════════════════════════════════════════════════════════════════
248
- // buildChannelConfig returns the full `channels: { ... }` object
249
- // ═══════════════════════════════════════════════════════════════════════════════
250
- function buildChannelConfig(opts) {
251
- const { channelKey, isMultiBot, groupId, agentMetas = [], botName, agentId } = opts;
252
- const channels = {};
253
-
254
- if (channelKey === 'telegram') {
255
- const telegramConfig = {
256
- enabled: true,
257
- defaultAccount: 'default',
258
- dmPolicy: 'open',
259
- allowFrom: ['*'],
260
- replyToMode: 'first',
261
- reactionLevel: 'minimal',
262
- actions: {
263
- sendMessage: true,
264
- reactions: true,
265
- },
266
- accounts: {},
267
- };
268
-
269
- if (isMultiBot) {
270
- // Multiple accounts — each bot gets its own account keyed by accountId
271
- telegramConfig.accounts = {};
272
- for (const meta of agentMetas) {
273
- telegramConfig.accounts[meta.accountId || 'default'] = {
274
- botToken: meta.token || '<your_bot_token>',
275
- };
276
- }
277
- telegramConfig.groupPolicy = groupId ? 'allowlist' : 'open';
278
- telegramConfig.groupAllowFrom = ['*'];
279
- telegramConfig.groups = {
280
- [groupId || '*']: { enabled: true, requireMention: false },
281
- };
282
- } else {
283
- // Single bot
284
- telegramConfig.accounts = {
285
- default: {
286
- botToken: (agentMetas[0] && agentMetas[0].token) || '<your_bot_token>',
287
- },
288
- };
289
- }
290
-
291
- channels.telegram = telegramConfig;
292
- } else if (isZaloPersonal(channelKey)) {
293
- // Zalo Personal matches live Mkt/Williams configs
294
- channels.zalouser = {
295
- enabled: true,
296
- defaultAccount: 'default',
297
- accounts: {
298
- default: {
299
- dmPolicy: 'open',
300
- allowFrom: ['*'],
301
- groupPolicy: 'allowlist',
302
- groupAllowFrom: ['*'],
303
- },
304
- },
305
- dmPolicy: 'open',
306
- allowFrom: ['*'],
307
- groupPolicy: 'allowlist',
308
- groupAllowFrom: ['*'],
309
- historyLimit: 50,
310
- groups: {
311
- '*': { enabled: true, requireMention: false },
312
- },
313
- };
314
- } else if (channelKey === 'zalo-bot') {
315
- channels.zalo = { enabled: true, provider: 'official_account' };
316
- }
317
-
318
- return channels;
319
- }
320
-
321
-
322
- // ═══════════════════════════════════════════════════════════════════════════════
323
- // buildPluginsConfig returns { plugins: { ... } }
324
- // ═══════════════════════════════════════════════════════════════════════════════
325
- function buildPluginsConfig(opts) {
326
- const { channelKey, selectedSkills = [], botName = 'Bot', agentId = 'bot', hasBrowser = false } = opts;
327
-
328
- const entries = {};
329
-
330
- // memory-core with dreaming — always present
331
- entries['memory-core'] = {
332
- config: {
333
- dreaming: {
334
- enabled: selectedSkills.includes('memory'),
335
- },
336
- },
337
- };
338
-
339
- const allow = ['memory-core'];
340
-
341
- // Zalo Personal channel is native; install openclaw-zalo-mod manually via ClawHub when needed.
342
- if (isZaloPersonal(channelKey)) {
343
- allow.push('zalouser');
344
- }
345
-
346
- // DuckDuckGo search plugin for web-search
347
- if (selectedSkills.includes('web-search') || selectedSkills.includes('web_search')) {
348
- entries['duckduckgo'] = { enabled: true };
349
- if (!allow.includes('duckduckgo')) {
350
- allow.push('duckduckgo');
351
- }
352
- }
353
-
354
- // Browser automation depends on the bundled `browser` plugin, which provides the
355
- // browser-control service. With an allowlist in use it must be explicitly allowed,
356
- // otherwise browser control stays disabled ("browser control is disabled").
357
- if (hasBrowser && !allow.includes('browser')) {
358
- allow.push('browser');
359
- }
360
-
361
- const plugins = { entries };
362
- plugins.allow = allow;
363
- if (allow.length) plugins.bundledDiscovery = 'compat';
364
-
365
- return { plugins };
366
- }
367
-
368
-
369
- // ═══════════════════════════════════════════════════════════════════════════════
370
- // buildSkillsEntries — returns { slug: { enabled: true } } map
371
- // ═══════════════════════════════════════════════════════════════════════════════
372
- function buildSkillsEntries(skills, selectedSkillIds) {
373
- const entries = {};
374
- if (!skills || !selectedSkillIds) return entries;
375
-
376
- for (const skill of skills) {
377
- const skillId = skill.value || skill.id;
378
- if (!selectedSkillIds.includes(skillId)) continue;
379
- // Skills without a slug are native (browser, scheduler) — not in skills.entries
380
- const slug = skill.slug;
381
- if (!slug) continue;
382
- // Skip browser-automation slug (handled by browser config)
383
- if (slug === 'browser-automation') continue;
384
- entries[slug] = { enabled: true };
385
- }
386
-
387
- return entries;
388
- }
389
-
390
-
391
- // ═══════════════════════════════════════════════════════════════════════════════
392
- // buildExecApprovalsJson — exec-approvals.json content
393
- // ═══════════════════════════════════════════════════════════════════════════════
394
- function buildExecApprovalsJson(opts) {
395
- const { agentMetas = [] } = opts;
396
- const agentEntries = {};
397
- agentEntries.main = { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true };
398
- for (const meta of agentMetas) {
399
- agentEntries[meta.agentId] = { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true };
400
- }
401
- return {
402
- version: 1,
403
- defaults: { security: 'full', ask: 'off', askFallback: 'full' },
404
- agents: agentEntries,
405
- };
406
- }
407
-
408
-
409
- // ═══════════════════════════════════════════════════════════════════════════════
410
- // buildEnvFileContent — .env file content for a single bot
411
- // ═══════════════════════════════════════════════════════════════════════════════
412
- /**
413
- * @param {object} opts
414
- * @param {object} opts.provider - Provider metadata
415
- * @param {string} opts.providerKeyVal - API key value
416
- * @param {string} opts.channelKey - Channel type
417
- * @param {string} opts.botToken - Bot token
418
- * @param {boolean} opts.isMultiBot
419
- * @param {string} opts.groupId
420
- * @param {Array} opts.selectedSkills
421
- * @param {string} opts.ttsOpenaiKey
422
- * @param {string} opts.ttsElevenKey
423
- * @param {string} opts.smtpHost
424
- * @param {string} opts.smtpPort
425
- * @param {string} opts.smtpUser
426
- * @param {string} opts.smtpPass
427
- * @param {boolean} opts.isSharedEnv - If true, omit per-bot token (multi-bot shared .env)
428
- */
429
- function buildEnvFileContent(opts) {
430
- const {
431
- provider = {},
432
- providerKeyVal = '',
433
- channelKey = 'telegram',
434
- botToken = '',
435
- isMultiBot = false,
436
- groupId = '',
437
- selectedSkills = [],
438
- ttsOpenaiKey = '',
439
- ttsElevenKey = '',
440
- smtpHost = 'smtp.gmail.com',
441
- smtpPort = '587',
442
- smtpUser = '',
443
- smtpPass = '',
444
- isSharedEnv = false,
445
- } = opts;
446
-
447
- const lines = [];
448
-
449
- if (provider.isLocal) {
450
- lines.push('OLLAMA_HOST=http://localhost:11434');
451
- lines.push('OLLAMA_API_KEY=ollama-local');
452
- } else if (provider.isProxy) {
453
- lines.push('# 9Router: no API key needed');
454
- } else if (provider.envKey) {
455
- lines.push(`${provider.envKey}=${providerKeyVal || '<your_api_key>'}`);
456
- }
457
-
458
- if (!isSharedEnv) {
459
- if (channelKey === 'telegram') {
460
- lines.push(`TELEGRAM_BOT_TOKEN=${botToken || '<your_bot_token>'}`);
461
- if (isMultiBot && groupId) lines.push(`TELEGRAM_GROUP_ID=${groupId}`);
462
- } else if (channelKey === 'zalo-bot') {
463
- lines.push('ZALO_APP_ID=');
464
- lines.push('ZALO_APP_SECRET=');
465
- lines.push(`ZALO_BOT_TOKEN=${botToken || '<your_zalo_bot_token>'}`);
466
- }
467
- }
468
-
469
- if (selectedSkills.includes('tts')) {
470
- lines.push('');
471
- lines.push('# --- Text-To-Speech ---');
472
- if (ttsOpenaiKey) lines.push(`OPENAI_API_KEY=${ttsOpenaiKey}`);
473
- if (ttsElevenKey) lines.push(`ELEVENLABS_API_KEY=${ttsElevenKey}`);
474
- }
475
-
476
- if (selectedSkills.includes('email')) {
477
- lines.push('');
478
- lines.push('# --- Email ---');
479
- lines.push(`SMTP_HOST=${smtpHost}`);
480
- lines.push(`SMTP_PORT=${smtpPort}`);
481
- lines.push(`SMTP_USER=${smtpUser}`);
482
- lines.push(`SMTP_PASS=${smtpPass}`);
483
- }
484
-
485
- return lines.join('\n') + '\n';
486
- }
487
-
488
-
489
- // ═══════════════════════════════════════════════════════════════════════════════
490
- // Export
491
- // ═══════════════════════════════════════════════════════════════════════════════
492
- const exports = {
493
- slugify,
494
- isZaloPersonal,
495
- generateToken,
496
- buildOpenclawJson,
497
- buildChannelConfig,
498
- buildPluginsConfig,
499
- buildSkillsEntries,
500
- buildExecApprovalsJson,
501
- buildEnvFileContent,
502
- };
503
-
504
- if (typeof root !== 'undefined') {
505
- root.__openclawBotConfig = exports;
506
- }
507
-
508
- })(typeof globalThis !== 'undefined' ? globalThis : {});
509
- if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawBotConfig) {
510
- Object.assign(exports, globalThis.__openclawBotConfig);
511
- }
1
+ // @ts-nocheck
2
+ /**
3
+ * @fileoverview Centralized bot configuration builders — single source of truth.
4
+ *
5
+ * Generates openclaw.json, auth-profiles.json, exec-approvals.json, and .env content.
6
+ * Used by BOTH the Wizard (IIFE bundle) and CLI (CJS require).
7
+ *
8
+ * Pattern: same as common-gen.js / workspace-gen.js — IIFE + CJS dual export.
9
+ */
10
+ (function (root) {
11
+
12
+ const _common = (typeof root !== 'undefined' && root.__openclawCommon) || {};
13
+
14
+ // ── Helper: slugify a bot name into a safe agent ID ─────────────────────────
15
+ function slugify(name) {
16
+ return String(name || 'bot').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'bot';
17
+ }
18
+
19
+ // ── Helper: detect if channel is zalo personal ───────────────────────────────
20
+ function isZaloPersonal(channelKey) {
21
+ return channelKey === 'zalo-personal';
22
+ }
23
+
24
+ // ── Helper: generate a random token (works in both browser + Node) ──────────
25
+ function generateToken() {
26
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
27
+ return crypto.randomUUID().replace(/-/g, '');
28
+ }
29
+ // Fallback for older Node.js
30
+ const hex = '0123456789abcdef';
31
+ let result = '';
32
+ for (let i = 0; i < 32; i++) result += hex[Math.floor(Math.random() * 16)];
33
+ return result;
34
+ }
35
+
36
+
37
+ // ═══════════════════════════════════════════════════════════════════════════════
38
+ // buildOpenclawJson — the ONE function that generates the full openclaw.json
39
+ // ═══════════════════════════════════════════════════════════════════════════════
40
+ /**
41
+ * @param {object} opts
42
+ * @param {string} opts.channelKey - 'telegram' | 'zalo-personal' | 'zalo-bot'
43
+ * @param {string} opts.deployMode - 'docker' | 'native'
44
+ * @param {string} opts.providerKey - '9router' | 'openai' | 'ollama' | ...
45
+ * @param {object} opts.provider - Provider metadata object from PROVIDERS
46
+ * @param {string} opts.model - Primary model ID (e.g. 'smart-route', 'gemma4:e2b')
47
+ * @param {boolean} opts.isMultiBot - Multi-bot mode
48
+ * @param {Array} opts.agentMetas - [{ agentId, name, desc, persona, token, slashCmd, accountId, workspaceDir }]
49
+ * @param {string} opts.groupId - Telegram group ID (multi-bot only)
50
+ * @param {Array} opts.selectedSkills - ['browser', 'memory', 'scheduler', ...]
51
+ * @param {Array} opts.skills - Full SKILLS registry array
52
+ * @param {boolean} opts.hasBrowserDesktop - Browser desktop mode
53
+ * @param {boolean} opts.hasBrowserServer - Browser server mode
54
+ * @param {number} [opts.gatewayPort=18789]
55
+ * @param {Array} [opts.gatewayAllowedOrigins]
56
+ * @param {string} [opts.osChoice] - 'windows' | 'macos' | 'vps' | 'ubuntu'
57
+ * @param {string} [opts.selectedModel] - For Ollama: specific model selected
58
+ */
59
+ function buildOpenclawJson(opts) {
60
+ const {
61
+ channelKey = 'telegram',
62
+ deployMode = 'docker',
63
+ providerKey = '9router',
64
+ provider = {},
65
+ model = 'smart-route',
66
+ isMultiBot = false,
67
+ agentMetas = [],
68
+ groupId = '',
69
+ selectedSkills = [],
70
+ skills = [],
71
+ hasBrowserDesktop = false,
72
+ hasBrowserServer = false,
73
+ gatewayPort = 18789,
74
+ gatewayAllowedOrigins = [],
75
+ osChoice = '',
76
+ selectedModel = '',
77
+ routerPort,
78
+ } = opts;
79
+
80
+ const common = _common;
81
+ const is9Router = providerKey === '9router';
82
+ const isLocal = !!provider.isLocal;
83
+
84
+ // ── agents ────────────────────────────────────────────────────────────────
85
+ const agentsList = agentMetas.map((meta) => ({
86
+ id: meta.agentId,
87
+ ...(meta.name ? { name: meta.name } : {}),
88
+ workspace: `/home/node/project/.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
89
+ agentDir: `agents/${meta.agentId}/agent`,
90
+ model: { primary: model, fallbacks: [] },
91
+ }));
92
+
93
+ const cfg = {
94
+ // NOTE: do NOT seed `lastTouchedVersion` here. OPENCLAW_NPM_SPEC is a range/`latest`
95
+ // (e.g. `>=2026.6.10`), not a concrete version — writing it makes OpenClaw fail to parse
96
+ // it on boot and crash the container. OpenClaw stamps the correct
97
+ // `{ lastTouchedVersion, lastTouchedAt }` itself on first run.
98
+ meta: {
99
+ osChoice,
100
+ deployMode,
101
+ },
102
+ agents: {
103
+ defaults: {
104
+ model: { primary: model, fallbacks: [] },
105
+ compaction: { mode: 'safeguard' },
106
+ // Trim stale tool results before Anthropic's prompt-cache TTL expires so the
107
+ // re-cache write stays small → lower token cost on long sessions, zero downside.
108
+ // The stable system prompt (AGENTS.md/SOUL.md…) stays cached above the boundary.
109
+ contextPruning: { mode: 'cache-ttl', ttl: '5m' },
110
+ // Agent-TURN budget (seconds). 120 was too short for multi-step cloud turns (OCR +
111
+ // file gen + tool calls). 900 = OpenClaw's own native default and the practical max
112
+ // we use. This is the turn budget — a DIFFERENT layer from 9router's per-request
113
+ // timeout, so raising it does not conflict with / overrun 9router.
114
+ timeoutSeconds: 900,
115
+ ...(isLocal ? { llm: { idleTimeoutSeconds: 300 } } : {}),
116
+ },
117
+ list: agentsList,
118
+ },
119
+ };
120
+
121
+ // ── models.providers ──────────────────────────────────────────────────────
122
+ if (is9Router && common.build9RouterProviderConfig) {
123
+ cfg.models = {
124
+ mode: 'merge',
125
+ providers: {
126
+ '9router': common.build9RouterProviderConfig(
127
+ common.get9RouterBaseUrl ? common.get9RouterBaseUrl(deployMode, routerPort) : `http://9router:${routerPort || 20128}/v1`
128
+ ),
129
+ },
130
+ };
131
+ } else if (isLocal) {
132
+ const ollamaBaseUrl = deployMode === 'docker' ? 'http://ollama:11434' : 'http://localhost:11434';
133
+ const OLLAMA_MODELS = (typeof root !== 'undefined' && root.__openclawData && root.__openclawData.OLLAMA_MODELS)
134
+ || (typeof _OLLAMA_MODELS !== 'undefined' ? _OLLAMA_MODELS : []);
135
+ const modelList = selectedModel
136
+ ? [{ id: selectedModel, name: selectedModel, contextWindow: 128000, maxTokens: 8192 }]
137
+ : OLLAMA_MODELS;
138
+ cfg.models = {
139
+ mode: 'merge',
140
+ providers: {
141
+ ollama: {
142
+ baseUrl: ollamaBaseUrl,
143
+ api: 'ollama',
144
+ apiKey: 'ollama-local',
145
+ models: modelList,
146
+ },
147
+ },
148
+ };
149
+ }
150
+
151
+ // ── commands ──────────────────────────────────────────────────────────────
152
+ cfg.commands = { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' };
153
+ if (selectedSkills.includes('scheduler')) {
154
+ cfg.commands.ownerAllowFrom = ['*'];
155
+ }
156
+
157
+ // ── bindings (multi-bot or Zalo) ─────────────────────────────────────────
158
+ if (agentMetas.length > 0 && isMultiBot && channelKey === 'telegram') {
159
+ cfg.bindings = agentMetas.map((meta) => ({
160
+ agentId: meta.agentId,
161
+ match: { channel: 'telegram', accountId: meta.accountId || 'default' },
162
+ }));
163
+ } else {
164
+ cfg.bindings = [];
165
+ }
166
+
167
+ // ── channels ─────────────────────────────────────────────────────────────
168
+ if (agentMetas.length > 0) {
169
+ cfg.channels = buildChannelConfig({
170
+ channelKey, isMultiBot, groupId, agentMetas, botName: agentMetas[0]?.name || 'Bot',
171
+ agentId: agentMetas[0]?.agentId || 'bot',
172
+ });
173
+ } else {
174
+ cfg.channels = {};
175
+ }
176
+
177
+ // ── tools ────────────────────────────────────────────────────────────────
178
+ cfg.tools = { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
179
+ const alsoAllow = [];
180
+ if (selectedSkills.includes('scheduler') || selectedSkills.includes('cron')) {
181
+ alsoAllow.push('group:automation');
182
+ }
183
+ if (
184
+ selectedSkills.includes('web-search') ||
185
+ selectedSkills.includes('web_search') ||
186
+ selectedSkills.includes('browser') ||
187
+ selectedSkills.includes('browser-automation') ||
188
+ hasBrowserDesktop ||
189
+ hasBrowserServer
190
+ ) {
191
+ alsoAllow.push('group:web');
192
+ }
193
+ if (alsoAllow.length > 0) {
194
+ cfg.tools.alsoAllow = alsoAllow;
195
+ }
196
+ // DuckDuckGo is the bundled, credential-free web_search provider. Auto-detect only
197
+ // picks providers that have credentials, so a free provider must be selected
198
+ // explicitly, otherwise web_search reports "no provider is available".
199
+ if (selectedSkills.includes('web-search') || selectedSkills.includes('web_search')) {
200
+ cfg.tools.web = {
201
+ ...(cfg.tools.web || {}),
202
+ search: { ...((cfg.tools.web && cfg.tools.web.search) || {}), provider: 'duckduckgo' },
203
+ };
204
+ }
205
+ if (isMultiBot) {
206
+ cfg.tools.agentToAgent = {
207
+ enabled: true,
208
+ allow: agentMetas.map((meta) => meta.agentId),
209
+ };
210
+ }
211
+
212
+ // ── gateway ──────────────────────────────────────────────────────────────
213
+ cfg.gateway = {
214
+ port: gatewayPort,
215
+ mode: 'local',
216
+ bind: (deployMode === 'docker' || osChoice === 'vps') ? 'custom' : 'loopback',
217
+ ...(deployMode === 'docker' || osChoice === 'vps' ? { customBindHost: '0.0.0.0' } : {}),
218
+ controlUi: {
219
+ allowedOrigins: gatewayAllowedOrigins.length > 0
220
+ ? gatewayAllowedOrigins
221
+ : [`http://localhost:${gatewayPort}`, `http://127.0.0.1:${gatewayPort}`, `http://0.0.0.0:${gatewayPort}`],
222
+ },
223
+ auth: { mode: 'token', token: generateToken() },
224
+ };
225
+
226
+ // ── browser (delegated to browser-automation plugin) ────────────────────
227
+
228
+ // ── skills ───────────────────────────────────────────────────────────────
229
+ const skillEntries = buildSkillsEntries(skills, selectedSkills);
230
+ if (Object.keys(skillEntries).length > 0) {
231
+ cfg.skills = { entries: skillEntries };
232
+ }
233
+
234
+ // ── plugins (memory-core dreaming + openclaw-zalo-mod) ────────────────────────────
235
+ const pluginsConfig = buildPluginsConfig({
236
+ channelKey,
237
+ selectedSkills,
238
+ botName: agentMetas[0]?.name || 'Bot',
239
+ agentId: agentMetas[0]?.agentId || 'bot',
240
+ hasBrowser: hasBrowserDesktop || hasBrowserServer
241
+ || selectedSkills.includes('browser') || selectedSkills.includes('browser-automation'),
242
+ });
243
+ cfg.plugins = pluginsConfig.plugins;
244
+
245
+ // ── bindings for zalouser ────────────────────────────────────────────────
246
+ if (agentMetas.length > 0 && isZaloPersonal(channelKey)) {
247
+ cfg.bindings = cfg.bindings || [];
248
+ const firstAgentId = agentMetas[0]?.agentId || 'bot';
249
+ if (!cfg.bindings.some(b => b.match && b.match.channel === 'zalouser')) {
250
+ // Account-specific from the start so a second Zalo account added later routes
251
+ // correctly (each zalouser account binds by accountId).
252
+ cfg.bindings.push({ agentId: firstAgentId, match: { channel: 'zalouser', accountId: 'default' } });
253
+ }
254
+ }
255
+
256
+ return cfg;
257
+ }
258
+
259
+
260
+ // ═══════════════════════════════════════════════════════════════════════════════
261
+ // buildChannelConfig — returns the full `channels: { ... }` object
262
+ // ═══════════════════════════════════════════════════════════════════════════════
263
+ function buildChannelConfig(opts) {
264
+ const { channelKey, isMultiBot, groupId, agentMetas = [], botName, agentId } = opts;
265
+ const channels = {};
266
+
267
+ if (channelKey === 'telegram') {
268
+ const telegramConfig = {
269
+ enabled: true,
270
+ defaultAccount: 'default',
271
+ dmPolicy: 'open',
272
+ allowFrom: ['*'],
273
+ replyToMode: 'first',
274
+ reactionLevel: 'minimal',
275
+ actions: {
276
+ sendMessage: true,
277
+ reactions: true,
278
+ },
279
+ accounts: {},
280
+ };
281
+
282
+ if (isMultiBot) {
283
+ // Multiple accounts — each bot gets its own account keyed by accountId
284
+ telegramConfig.accounts = {};
285
+ for (const meta of agentMetas) {
286
+ telegramConfig.accounts[meta.accountId || 'default'] = {
287
+ botToken: meta.token || '<your_bot_token>',
288
+ };
289
+ }
290
+ telegramConfig.groupPolicy = groupId ? 'allowlist' : 'open';
291
+ telegramConfig.groupAllowFrom = ['*'];
292
+ telegramConfig.groups = {
293
+ [groupId || '*']: { enabled: true, requireMention: false },
294
+ };
295
+ } else {
296
+ // Single bot
297
+ telegramConfig.accounts = {
298
+ default: {
299
+ botToken: (agentMetas[0] && agentMetas[0].token) || '<your_bot_token>',
300
+ },
301
+ };
302
+ }
303
+
304
+ channels.telegram = telegramConfig;
305
+ } else if (isZaloPersonal(channelKey)) {
306
+ // Zalo Personal — matches live Mkt/Williams configs
307
+ channels.zalouser = {
308
+ enabled: true,
309
+ defaultAccount: 'default',
310
+ accounts: {
311
+ default: {
312
+ dmPolicy: 'open',
313
+ allowFrom: ['*'],
314
+ groupPolicy: 'allowlist',
315
+ groupAllowFrom: ['*'],
316
+ },
317
+ },
318
+ dmPolicy: 'open',
319
+ allowFrom: ['*'],
320
+ groupPolicy: 'allowlist',
321
+ groupAllowFrom: ['*'],
322
+ historyLimit: 50,
323
+ // NOTE: do NOT add `reactionLevel`/`actions` here the zalouser plugin's config
324
+ // schema is strict and rejects them ("must not have additional properties"), which
325
+ // crashes the gateway on boot. zalouser already supports the `react` action by
326
+ // default; reaction behavior is driven by the prompt (TOOLS.md), not channel config.
327
+ groups: {
328
+ '*': { enabled: true, requireMention: false },
329
+ },
330
+ };
331
+ } else if (channelKey === 'zalo-bot') {
332
+ channels.zalo = { enabled: true, provider: 'official_account' };
333
+ }
334
+
335
+ return channels;
336
+ }
337
+
338
+
339
+ // ═══════════════════════════════════════════════════════════════════════════════
340
+ // buildPluginsConfig — returns { plugins: { ... } }
341
+ // ═══════════════════════════════════════════════════════════════════════════════
342
+ function buildPluginsConfig(opts) {
343
+ const { channelKey, selectedSkills = [], botName = 'Bot', agentId = 'bot', hasBrowser = false } = opts;
344
+
345
+ const entries = {};
346
+
347
+ // memory-core with dreaming — always present
348
+ entries['memory-core'] = {
349
+ config: {
350
+ dreaming: {
351
+ enabled: selectedSkills.includes('memory'),
352
+ },
353
+ },
354
+ };
355
+
356
+ const allow = ['memory-core'];
357
+
358
+ // Zalo Personal channel is native; install openclaw-zalo-mod manually via ClawHub when needed.
359
+ if (isZaloPersonal(channelKey)) {
360
+ allow.push('zalouser');
361
+ }
362
+
363
+ // DuckDuckGo search plugin for web-search
364
+ if (selectedSkills.includes('web-search') || selectedSkills.includes('web_search')) {
365
+ entries['duckduckgo'] = { enabled: true };
366
+ if (!allow.includes('duckduckgo')) {
367
+ allow.push('duckduckgo');
368
+ }
369
+ }
370
+
371
+ // Browser automation depends on the bundled `browser` plugin, which provides the
372
+ // browser-control service. With an allowlist in use it must be explicitly allowed,
373
+ // otherwise browser control stays disabled ("browser control is disabled").
374
+ if (hasBrowser && !allow.includes('browser')) {
375
+ allow.push('browser');
376
+ }
377
+
378
+ const plugins = { entries };
379
+ plugins.allow = allow;
380
+ if (allow.length) plugins.bundledDiscovery = 'compat';
381
+
382
+ return { plugins };
383
+ }
384
+
385
+
386
+ // ═══════════════════════════════════════════════════════════════════════════════
387
+ // buildSkillsEntries — returns { slug: { enabled: true } } map
388
+ // ═══════════════════════════════════════════════════════════════════════════════
389
+ function buildSkillsEntries(skills, selectedSkillIds) {
390
+ const entries = {};
391
+ if (!skills || !selectedSkillIds) return entries;
392
+
393
+ for (const skill of skills) {
394
+ const skillId = skill.value || skill.id;
395
+ if (!selectedSkillIds.includes(skillId)) continue;
396
+ // Skills without a slug are native (browser, scheduler) — not in skills.entries
397
+ const slug = skill.slug;
398
+ if (!slug) continue;
399
+ // Skip browser-automation slug (handled by browser config)
400
+ if (slug === 'browser-automation') continue;
401
+ entries[slug] = { enabled: true };
402
+ }
403
+
404
+ return entries;
405
+ }
406
+
407
+
408
+ // ═══════════════════════════════════════════════════════════════════════════════
409
+ // buildExecApprovalsJson — exec-approvals.json content
410
+ // ═══════════════════════════════════════════════════════════════════════════════
411
+ function buildExecApprovalsJson(opts) {
412
+ const { agentMetas = [] } = opts;
413
+ const agentEntries = {};
414
+ agentEntries.main = { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true };
415
+ for (const meta of agentMetas) {
416
+ agentEntries[meta.agentId] = { security: 'full', ask: 'off', askFallback: 'full', autoAllowSkills: true };
417
+ }
418
+ return {
419
+ version: 1,
420
+ defaults: { security: 'full', ask: 'off', askFallback: 'full' },
421
+ agents: agentEntries,
422
+ };
423
+ }
424
+
425
+
426
+ // ═══════════════════════════════════════════════════════════════════════════════
427
+ // buildEnvFileContent .env file content for a single bot
428
+ // ═══════════════════════════════════════════════════════════════════════════════
429
+ /**
430
+ * @param {object} opts
431
+ * @param {object} opts.provider - Provider metadata
432
+ * @param {string} opts.providerKeyVal - API key value
433
+ * @param {string} opts.channelKey - Channel type
434
+ * @param {string} opts.botToken - Bot token
435
+ * @param {boolean} opts.isMultiBot
436
+ * @param {string} opts.groupId
437
+ * @param {Array} opts.selectedSkills
438
+ * @param {string} opts.ttsOpenaiKey
439
+ * @param {string} opts.ttsElevenKey
440
+ * @param {string} opts.smtpHost
441
+ * @param {string} opts.smtpPort
442
+ * @param {string} opts.smtpUser
443
+ * @param {string} opts.smtpPass
444
+ * @param {boolean} opts.isSharedEnv - If true, omit per-bot token (multi-bot shared .env)
445
+ */
446
+ function buildEnvFileContent(opts) {
447
+ const {
448
+ provider = {},
449
+ providerKeyVal = '',
450
+ channelKey = 'telegram',
451
+ botToken = '',
452
+ isMultiBot = false,
453
+ groupId = '',
454
+ selectedSkills = [],
455
+ ttsOpenaiKey = '',
456
+ ttsElevenKey = '',
457
+ smtpHost = 'smtp.gmail.com',
458
+ smtpPort = '587',
459
+ smtpUser = '',
460
+ smtpPass = '',
461
+ isSharedEnv = false,
462
+ } = opts;
463
+
464
+ const lines = [];
465
+
466
+ if (provider.isLocal) {
467
+ lines.push('OLLAMA_HOST=http://localhost:11434');
468
+ lines.push('OLLAMA_API_KEY=ollama-local');
469
+ } else if (provider.isProxy) {
470
+ lines.push('# 9Router: no API key needed');
471
+ } else if (provider.envKey) {
472
+ lines.push(`${provider.envKey}=${providerKeyVal || '<your_api_key>'}`);
473
+ }
474
+
475
+ if (!isSharedEnv) {
476
+ if (channelKey === 'telegram') {
477
+ lines.push(`TELEGRAM_BOT_TOKEN=${botToken || '<your_bot_token>'}`);
478
+ if (isMultiBot && groupId) lines.push(`TELEGRAM_GROUP_ID=${groupId}`);
479
+ } else if (channelKey === 'zalo-bot') {
480
+ lines.push('ZALO_APP_ID=');
481
+ lines.push('ZALO_APP_SECRET=');
482
+ lines.push(`ZALO_BOT_TOKEN=${botToken || '<your_zalo_bot_token>'}`);
483
+ }
484
+ }
485
+
486
+ if (selectedSkills.includes('tts')) {
487
+ lines.push('');
488
+ lines.push('# --- Text-To-Speech ---');
489
+ if (ttsOpenaiKey) lines.push(`OPENAI_API_KEY=${ttsOpenaiKey}`);
490
+ if (ttsElevenKey) lines.push(`ELEVENLABS_API_KEY=${ttsElevenKey}`);
491
+ }
492
+
493
+ if (selectedSkills.includes('email')) {
494
+ lines.push('');
495
+ lines.push('# --- Email ---');
496
+ lines.push(`SMTP_HOST=${smtpHost}`);
497
+ lines.push(`SMTP_PORT=${smtpPort}`);
498
+ lines.push(`SMTP_USER=${smtpUser}`);
499
+ lines.push(`SMTP_PASS=${smtpPass}`);
500
+ }
501
+
502
+ return lines.join('\n') + '\n';
503
+ }
504
+
505
+
506
+ // ═══════════════════════════════════════════════════════════════════════════════
507
+ // Export
508
+ // ═══════════════════════════════════════════════════════════════════════════════
509
+ const exports = {
510
+ slugify,
511
+ isZaloPersonal,
512
+ generateToken,
513
+ buildOpenclawJson,
514
+ buildChannelConfig,
515
+ buildPluginsConfig,
516
+ buildSkillsEntries,
517
+ buildExecApprovalsJson,
518
+ buildEnvFileContent,
519
+ };
520
+
521
+ if (typeof root !== 'undefined') {
522
+ root.__openclawBotConfig = exports;
523
+ }
524
+
525
+ })(typeof globalThis !== 'undefined' ? globalThis : {});
526
+ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawBotConfig) {
527
+ Object.assign(exports, globalThis.__openclawBotConfig);
528
+ }