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