create-openclaw-bot 5.11.0 → 5.12.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.
@@ -55,31 +55,30 @@
55
55
  },
56
56
  // 'telegram+zalo-personal' — Combo mode tạm ngưng, nghiên cứu thêm.
57
57
  'zalo-personal': {
58
- name: 'Zalo Personal',
58
+ name: 'Zalo cá nhân — OpenClaw Zalo Connect',
59
59
  hasZaloPersonal: true,
60
60
  envKeys: [],
61
61
  envExtra: '',
62
62
  credSteps: [
63
- { textVi: '⚠️ Zalo Personal dùng <strong>unofficial API (zca-js)</strong> — chỉ nên dùng tài khoản phụ', textEn: '⚠️ Zalo Personal uses <strong>unofficial API (zca-js)</strong> — use an alternate account' },
64
- { textVi: 'Native setup sẽ tự chạy login copy QR về thư mục project. Nếu cần chạy lại thủ công, dùng <code>openclaw channels login --channel zalouser --verbose</code>.', textEn: 'Native setup now auto-runs the login flow and copies the QR into the project folder. If needed, rerun it manually with <code>openclaw channels login --channel zalouser --verbose</code>.' },
63
+ { textVi: '⚠️ Zalo nhân chạy qua <strong>OpenClaw Zalo Connect (unofficial, zca-js)</strong> — tự động hoá tài khoản cá nhân có thể vi phạm điều khoản Zalo và khiến tài khoản bị hạn chế. <strong>Chỉ nên dùng tài khoản phụ.</strong>', textEn: '⚠️ Personal Zalo runs on <strong>OpenClaw Zalo Connect (unofficial, zca-js)</strong> — automating a personal account may violate Zalo terms and can get the account restricted. <strong>Use a secondary account.</strong>' },
64
+ { textVi: 'Sau khi tạo bot, bấm <strong>Đăng nhập Zalo</strong> để quét QR ngay trong giao diện. Nếu cần chạy thủ công: <code>openclaw channels login --channel zalo-connect --account default</code>.', textEn: 'After creating the bot, click <strong>Zalo Login</strong> to scan the QR right in the UI. Manual fallback: <code>openclaw channels login --channel zalo-connect --account default</code>.' },
65
+ { textVi: 'Mặc định an toàn: DM theo cơ chế pairing, group tắt hết — sau khi quét QR hãy tự chọn DM/nhóm được phép trong cấu hình.', textEn: 'Secure defaults: DM uses pairing, all groups start disabled — after the QR scan, explicitly choose the allowed DMs/groups in the config.' },
65
66
  ],
67
+ // Mirrors buildZaloConnectChannelConfig() in bot-config-gen.js — keys validated
68
+ // against OpenClaw Zalo Connect 3.0.0's strict channel schema.
66
69
  channelConfig: {
67
- zalouser: {
70
+ 'zalo-connect': {
68
71
  enabled: true,
69
72
  defaultAccount: 'default',
70
73
  accounts: {
71
- default: {
72
- dmPolicy: 'open',
73
- allowFrom: ['*'],
74
- groupPolicy: 'allowlist',
75
- groupAllowFrom: ['*'],
76
- },
74
+ default: { enabled: true },
77
75
  },
78
- dmPolicy: 'open',
79
- allowFrom: ['*'],
76
+ dmPolicy: 'pairing',
77
+ allowFrom: [],
80
78
  groupPolicy: 'allowlist',
81
- groupAllowFrom: ['*'],
82
- historyLimit: 50,
79
+ groups: {
80
+ '*': { enabled: false, requireMention: true },
81
+ },
83
82
  },
84
83
  },
85
84
  pluginInstall: '',
@@ -248,14 +248,14 @@
248
248
  });
249
249
  cfg.plugins = pluginsConfig.plugins;
250
250
 
251
- // ── bindings for zalouser ────────────────────────────────────────────────
251
+ // ── bindings for zalo-connect ────────────────────────────────────────────────
252
252
  if (agentMetas.length > 0 && isZaloPersonal(channelKey)) {
253
253
  cfg.bindings = cfg.bindings || [];
254
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' } });
255
+ if (!cfg.bindings.some(b => b.match && b.match.channel === 'zalo-connect')) {
256
+ // Account-specific from day one (plan §5.2): each account needs its own
257
+ // binding + credential namespace even while zalo-connect 2.4.6 is single-account.
258
+ cfg.bindings.push({ agentId: firstAgentId, match: { channel: 'zalo-connect', accountId: 'default' } });
259
259
  }
260
260
  }
261
261
 
@@ -309,31 +309,15 @@
309
309
 
310
310
  channels.telegram = telegramConfig;
311
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
- };
312
+ // Zalo Personal OpenClaw Zalo Connect, the only personal-Zalo backend.
313
+ // Secure defaults: DM pairing, empty
314
+ // allowFrom, all groups disabled + requireMention until the owner explicitly
315
+ // enables them post-QR. Never default to `allowFrom: ["*"]` / open groups.
316
+ // Every key below is validated against OpenClaw Zalo Connect 3.0.0's schema
317
+ // (`additionalProperties: false`) — do NOT add keys (e.g. historyLimit,
318
+ // groupAllowFrom) without re-checking `openclaw.plugin.json` first, an unknown
319
+ // key crashes the gateway on boot.
320
+ channels['zalo-connect'] = buildZaloConnectChannelConfig();
337
321
  } else if (channelKey === 'zalo-bot') {
338
322
  channels.zalo = { enabled: true, provider: 'official_account' };
339
323
  }
@@ -342,6 +326,26 @@
342
326
  }
343
327
 
344
328
 
329
+ // ═══════════════════════════════════════════════════════════════════════════════
330
+ // buildZaloConnectChannelConfig — secure-default channels['zalo-connect'] block
331
+ // ═══════════════════════════════════════════════════════════════════════════════
332
+ function buildZaloConnectChannelConfig() {
333
+ return {
334
+ enabled: true,
335
+ defaultAccount: 'default',
336
+ accounts: {
337
+ default: { enabled: true },
338
+ },
339
+ dmPolicy: 'pairing',
340
+ allowFrom: [],
341
+ groupPolicy: 'allowlist',
342
+ groups: {
343
+ '*': { enabled: false, requireMention: true },
344
+ },
345
+ };
346
+ }
347
+
348
+
345
349
  // ═══════════════════════════════════════════════════════════════════════════════
346
350
  // buildPluginsConfig — returns { plugins: { ... } }
347
351
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -361,9 +365,11 @@
361
365
 
362
366
  const allow = ['memory-core'];
363
367
 
364
- // Zalo Personal channel is native; install openclaw-zalo-mod manually via ClawHub when needed.
368
+ // Zalo Personal → zalo-connect channel plugin (pinned install happens in the
369
+ // entrypoint / creation flow). openclaw-zalo-mod stays a separate opt-in plugin.
365
370
  if (isZaloPersonal(channelKey)) {
366
- allow.push('zalouser');
371
+ entries['zalo-connect'] = { enabled: true };
372
+ allow.push('zalo-connect');
367
373
  }
368
374
 
369
375
  // DuckDuckGo search plugin for web-search
@@ -518,6 +524,7 @@
518
524
  generateToken,
519
525
  buildOpenclawJson,
520
526
  buildChannelConfig,
527
+ buildZaloConnectChannelConfig,
521
528
  buildPluginsConfig,
522
529
  buildSkillsEntries,
523
530
  buildExecApprovalsJson,
@@ -40,6 +40,13 @@
40
40
  const TELEGRAM_RELAY_PLUGIN_ID = 'telegram-multibot-relay';
41
41
  const TELEGRAM_SETUP_GUIDE_FILENAME = 'TELEGRAM-GROUP-SETUP.md';
42
42
 
43
+ // ── OpenClaw Zalo Connect — single source of truth ──────────────────────────
44
+ // Pin the maintained fork tag so setup never falls back to upstream/ClawHub.
45
+ const ZALO_CHANNEL_ID = 'zalo-connect';
46
+ const ZALO_PLUGIN_ID = 'zalo-connect';
47
+ const ZALO_CONNECT_VERSION = '3.0.0';
48
+ const ZALO_CONNECT_PLUGIN_SPEC = 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.0';
49
+
43
50
  function buildRelayPluginInstallCommand(prefix = 'openclaw') {
44
51
  return `if [ ! -d "$OPENCLAW_STATE_DIR/extensions/${TELEGRAM_RELAY_PLUGIN_ID}" ]; then ${prefix} plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC} 2>/dev/null || true; fi`;
45
52
  }
@@ -298,6 +305,10 @@ If setup reported a plugin install error, run this after the bot is running:
298
305
  TELEGRAM_RELAY_PLUGIN_SPEC,
299
306
  TELEGRAM_RELAY_PLUGIN_ID,
300
307
  TELEGRAM_SETUP_GUIDE_FILENAME,
308
+ ZALO_CHANNEL_ID,
309
+ ZALO_PLUGIN_ID,
310
+ ZALO_CONNECT_VERSION,
311
+ ZALO_CONNECT_PLUGIN_SPEC,
301
312
  buildRelayPluginInstallCommand,
302
313
  buildRelayPluginInstallCommandWin,
303
314
  buildTelegramPostInstallChecklist,
@@ -223,6 +223,8 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
223
223
  gatewayPort = 18789,
224
224
  routerPort = 20128,
225
225
  osChoice = '',
226
+ // Personal-Zalo backend: empty or the single supported `zalo-connect` channel.
227
+ zaloBackend = '',
226
228
  } = options;
227
229
  // Windows bind-mounts give ClawHub-installed plugins world-writable perms (which openclaw
228
230
  // blocks), so on Windows we isolate extensions in a named volume. On macOS/Linux bind-mounts
@@ -231,6 +233,16 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
231
233
  const useExtensionsVolume = osChoice === 'win';
232
234
  const extVolMount = useExtensionsVolume ? '\n - openclaw-extensions:/home/node/project/.openclaw/extensions' : '';
233
235
  const extVolDecl = useExtensionsVolume ? '\n openclaw-extensions:' : '';
236
+ // SQLite state on Docker Desktop (macOS/Windows): the host bind mount goes through a
237
+ // virtualized file share (virtiofs/gRPC-FUSE) whose locking/mmap semantics break SQLite WAL —
238
+ // OpenClaw crashes with `Error: disk I/O error` on write (e.g. when a Zalo message arrives).
239
+ // Keep `.openclaw/state` on a named volume (the Linux VM's native filesystem) instead; the
240
+ // rest of `.openclaw` stays bind-mounted so workspaces/config remain visible on the host.
241
+ // Linux/VPS bind mounts are native ext4 — unchanged there (and state stays host-visible).
242
+ const useStateVolume = osChoice === 'macos' || osChoice === 'win';
243
+ const stateVolMount = useStateVolume ? '\n - openclaw-state:/home/node/project/.openclaw/state' : '';
244
+ const stateVolDecl = useStateVolume ? '\n openclaw-state:' : '';
245
+ const stateVolBlock = useStateVolume ? '\n\nvolumes:\n openclaw-state:' : '';
234
246
  const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
235
247
  ? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
236
248
  : '';
@@ -268,21 +280,20 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
268
280
  ' return 0',
269
281
  ' fi',
270
282
  ' echo "[entrypoint] plugin $id missing; installing $spec"',
271
- ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
272
- '}',
273
- 'ensure_zalouser() {',
274
- ' NPM_DIR="$OPENCLAW_HOME/npm"',
275
- ' PKG_DIR="$NPM_DIR/node_modules/@openclaw/zalouser"',
276
- ' if [ -d "$PKG_DIR" ] || [ -d "$OPENCLAW_HOME/extensions/zalouser" ]; then',
277
- ' echo "[entrypoint] zalouser plugin already installed (npm or extensions) skipping"',
278
- ' else',
279
- ' echo "[entrypoint] zalouser plugin missing; installing via npm"',
280
- ' mkdir -p "$NPM_DIR"',
281
- ' cd "$NPM_DIR"',
282
- ' npm init -y 2>/dev/null || true',
283
- ' npm install @openclaw/zalouser@latest 2>/dev/null || echo "[entrypoint] warning: failed to install @openclaw/zalouser"',
284
- ' cd /home/node/project',
285
- ' fi',
283
+ ' case "$spec" in',
284
+ ' https://*.git#*)',
285
+ ' repo="${spec%%#*}"',
286
+ ' ref="${spec##*#}"',
287
+ ' tmp="/tmp/openclaw-plugin-$id-$ref"',
288
+ ' rm -rf "$tmp"',
289
+ ' if git clone --depth 1 --branch "$ref" "$repo" "$tmp" 2>/dev/null; then',
290
+ ' openclaw plugins install "$tmp" 2>/dev/null || echo "[entrypoint] warning: failed to install cloned plugin $id"',
291
+ ' else',
292
+ ' echo "[entrypoint] warning: failed to clone plugin $spec"',
293
+ ' fi',
294
+ ' ;;',
295
+ ' *) openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec" ;;',
296
+ ' esac',
286
297
  '}',
287
298
  'ensure_skill() {',
288
299
  ' id="$1"',
@@ -301,52 +312,14 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
301
312
  // Restore config AFTER plugin installs (which may clobber openclaw.json)
302
313
  runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
303
314
  runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
304
- // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
305
- runtimeParts.push([
306
- '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
307
- 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
308
- 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
309
- ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
310
- ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
311
- 'fi',
312
- '# Automatically run Zalo sticker-mention patch if skill is present',
313
- 'for MENTIONS_JS in $(find "$OPENCLAW_HOME" -maxdepth 5 -path "*/skills/*/mentions.js" -type f 2>/dev/null); do',
314
- ' if [ -f "$MENTIONS_JS" ]; then',
315
- ' echo "[entrypoint] Running patch: $MENTIONS_JS"',
316
- ' node "$MENTIONS_JS" || echo "[entrypoint] Warning: failed to run patch $MENTIONS_JS"',
317
- ' fi',
318
- 'done',
319
- ].join('\n'));
320
- // Expose zalouser's ZCA API map (globalThis.__zcaApiByProfile) BEFORE the gateway imports
321
- // zalouser, so openclaw-zalo-mod can reach the live Zalo API (dashboard Sync Account, group
322
- // admin lookups, …). zalo-mod's own runtime patch lands AFTER zalouser is already imported,
323
- // so it never takes effect on the running module — patching here (post sticker-mention restore,
324
- // pre gateway-run) is the reliable fix.
325
- {
326
- const exposeZcaScript = [
327
- "const fs=require('fs'),cp=require('child_process');",
328
- "let files=[];",
329
- "try{ files=cp.execSync('find \"'+(process.env.OPENCLAW_HOME||'')+'/npm\" -path \"*/@openclaw/zalouser/dist/zalo-js-*.js\" -type f 2>/dev/null',{encoding:'utf8'}).split('\\n').filter(Boolean); }catch(e){}",
330
- "const reps=[['const apiByProfile = /* @__PURE__ */ new Map();','const apiByProfile = globalThis.__zcaApiByProfile || (globalThis.__zcaApiByProfile = /* @__PURE__ */ new Map());'],['const apiByProfile = new Map();','const apiByProfile = globalThis.__zcaApiByProfile || (globalThis.__zcaApiByProfile = new Map());']];",
331
- "for(const f of files){ try{ let s=fs.readFileSync(f,'utf8'); if(s.includes('globalThis.__zcaApiByProfile')) continue; for(const pair of reps){ if(s.includes(pair[0])){ s=s.replace(pair[0],pair[1]); fs.writeFileSync(f,s); console.log('[entrypoint] exposed __zcaApiByProfile for zalo-mod in '+f); break; } } }catch(e){} }",
332
- ].join('\n');
333
- runtimeParts.push(`# Expose zalouser ZCA API map for openclaw-zalo-mod (before gateway imports zalouser)\nnode - <<'NODE'\n${exposeZcaScript}\nNODE`);
315
+ if (zaloBackend === 'zalo-connect') {
316
+ // Pinned ZaloConnect install (never `latest` — plan §4.1). ensure_plugin skips the
317
+ // install when extensions/zalo-connect already exists, so restarts never re-download
318
+ // and a reconnect never reinstalls. No dist patching, no mentions.js, no watchdog
319
+ // hacks: sticker/mention/reaction are native ZaloConnect actions.
320
+ const zaloConnectSpec = common.ZALO_CONNECT_PLUGIN_SPEC || 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.0';
321
+ runtimeParts.push(`ensure_plugin zalo-connect "${zaloConnectSpec}"`);
334
322
  }
335
- runtimeParts.push([
336
- '# Zalo channel auto-restart monitor (background)',
337
- '(',
338
- ' sleep 180',
339
- ' while true; do',
340
- ' sleep 60',
341
- ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
342
- ' if echo "$STATUS" | grep -qi "stopped"; then',
343
- ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
344
- ' sleep 5',
345
- ' kill 1 2>/dev/null || true',
346
- ' fi',
347
- ' done',
348
- ') &',
349
- ].join('\n'));
350
323
  runtimeParts.push('openclaw gateway run');
351
324
  const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
352
325
  let browserInstall = '';
@@ -395,7 +368,7 @@ services:
395
368
  env_file:
396
369
  - ../../.env
397
370
  ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
398
- - ${volumeMount}
371
+ - ${volumeMount}${stateVolMount}
399
372
  ports:
400
373
  - "127.0.0.1:${gatewayPort}:${gatewayPort}"
401
374
 
@@ -420,7 +393,7 @@ ${indentBlock(docker9RouterEntrypointScript, 8)}
420
393
  - "127.0.0.1:${routerPort}:${routerPort}"
421
394
 
422
395
  volumes:
423
- 9router-data:`;
396
+ 9router-data:${stateVolDecl}`;
424
397
  } else if (isLocal) {
425
398
  const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
426
399
  compose = `name: ${multiComposeName}
@@ -432,7 +405,7 @@ services:
432
405
  env_file:
433
406
  - ../../.env
434
407
  ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
435
- - ${volumeMount}
408
+ - ${volumeMount}${stateVolMount}
436
409
  ports:
437
410
  - "127.0.0.1:${gatewayPort}:${gatewayPort}"
438
411
 
@@ -461,7 +434,7 @@ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
461
434
  start_period: 30s
462
435
 
463
436
  volumes:
464
- ollama-data:`;
437
+ ollama-data:${stateVolDecl}`;
465
438
  } else {
466
439
  compose = `name: ${multiComposeName}
467
440
  services:
@@ -472,9 +445,9 @@ services:
472
445
  env_file:
473
446
  - ../../.env
474
447
  ${appEnvironmentBlock}${extraHosts} volumes:
475
- - ${volumeMount}
448
+ - ${volumeMount}${stateVolMount}
476
449
  ports:
477
- - "127.0.0.1:${gatewayPort}:${gatewayPort}"`;
450
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"${stateVolBlock}`;
478
451
  }
479
452
  } else if (is9Router) {
480
453
  compose = `name: ${singleComposeName}
@@ -488,7 +461,7 @@ services:
488
461
  depends_on:
489
462
  - 9router
490
463
  ${appEnvironmentBlock}${extraHostsBlock}\n volumes:
491
- - ${volumeMount}
464
+ - ${volumeMount}${stateVolMount}
492
465
  - openclaw-plugins:/home/node/project/.openclaw/npm${extVolMount}
493
466
  ports:
494
467
  - "127.0.0.1:${gatewayPort}:${gatewayPort}"
@@ -515,7 +488,7 @@ ${indentBlock(docker9RouterEntrypointScript, 8)}
515
488
 
516
489
  volumes:
517
490
  9router-data:
518
- openclaw-plugins:${extVolDecl}`;
491
+ openclaw-plugins:${extVolDecl}${stateVolDecl}`;
519
492
  } else if (isLocal) {
520
493
  const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
521
494
  compose = `name: ${singleComposeName}
@@ -531,7 +504,7 @@ ${appEnvironmentBlock} depends_on:
531
504
  ${extraHostsBlock}\n ports:
532
505
  - "127.0.0.1:${gatewayPort}:${gatewayPort}"
533
506
  volumes:
534
- - ${volumeMount}
507
+ - ${volumeMount}${stateVolMount}
535
508
 
536
509
  ollama:
537
510
  image: ollama/ollama:latest
@@ -558,7 +531,7 @@ ${extraHostsBlock}\n ports:
558
531
  start_period: 30s
559
532
 
560
533
  volumes:
561
- ollama-data:`;
534
+ ollama-data:${stateVolDecl}`;
562
535
  } else {
563
536
  compose = `name: ${singleComposeName}
564
537
  services:
@@ -569,9 +542,9 @@ services:
569
542
  env_file:
570
543
  - ../../.env
571
544
  ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
572
- - ${volumeMount}
545
+ - ${volumeMount}${stateVolMount}
573
546
  ports:
574
- - "127.0.0.1:${gatewayPort}:${gatewayPort}"`;
547
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"${stateVolBlock}`;
575
548
  }
576
549
 
577
550
  return {
@@ -599,5 +572,3 @@ ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''}
599
572
  if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
600
573
  Object.assign(exports, globalThis.__openclawDockerGen);
601
574
  }
602
-
603
-