create-openclaw-bot 5.7.10 → 5.8.1

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.
@@ -17,65 +17,106 @@
17
17
  return String(text).split('\n').map((line) => `${prefix}${line}`).join('\n');
18
18
  }
19
19
 
20
- function build9RouterSmartRouteSyncScript(dbPath) {
21
- return `const fs=require('fs');const INTERVAL=30000;const p='${dbPath}';
22
- const PM=${JSON.stringify(SMART_ROUTE_PROVIDER_MODELS)};
23
- const PREF=${JSON.stringify(SMART_ROUTE_PROVIDER_ORDER)};
24
- console.log('[sync-combo] 9Router sync loop started...');
25
- const sync = async () => {
26
- try {
27
- let db = {};
28
- try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
29
- if (!db.combos) db.combos = [];
30
- const removeSmartRoute = () => {
31
- const next = db.combos.filter(x => x.id !== 'smart-route');
32
- if (next.length !== db.combos.length) {
33
- db.combos = next;
34
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
35
- console.log('[sync-combo] Removed smart-route (no active providers)');
36
- }
37
- };
38
- const res = await fetch('http://localhost:20128/api/providers');
39
- if (!res.ok) { console.log('[sync-combo] API not ready, retrying...'); return; }
40
- const d = await res.json();
41
- const rawConnections = Array.isArray(d.connections) ? d.connections : Array.isArray(d.providerConnections) ? d.providerConnections : [];
42
- const activeConns = rawConnections.filter(c => c && c.provider && c.isActive !== false && !c.disabled);
43
- const a = [...new Set(activeConns.map(c => c.provider))];
44
- if (!a.length) { console.log('[sync-combo] No active providers reported; keeping existing smart-route'); return; }
45
- a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
46
- const m = [];
47
- for (const pv of a) {
48
- if (PM[pv]) m.push(...PM[pv]);
49
- const conns = activeConns.filter(c => c.provider === pv);
50
- for (const c of conns) {
51
- if (Array.isArray(c.models)) {
52
- for (const mdl of c.models) {
53
- const mdlId = typeof mdl === 'string' ? mdl : mdl.id;
54
- if (mdlId && !m.includes(mdlId) && !m.includes(pv + '/' + mdlId)) {
55
- m.push(pv + '/' + mdlId);
56
- }
57
- }
58
- }
59
- }
60
- }
61
- if (!m.length) { console.log('[sync-combo] No mapped models for active providers; keeping existing smart-route'); return; }
62
- const c = { id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m };
63
- const i = db.combos.findIndex(x => x.id === 'smart-route');
64
- if (i >= 0) {
65
- if (JSON.stringify(db.combos[i].models) !== JSON.stringify(c.models)) {
66
- db.combos[i] = c;
67
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
68
- console.log('[sync-combo] Updated smart-route: ' + c.models.length + ' models');
69
- }
70
- } else {
71
- db.combos.push(c);
72
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
73
- console.log('[sync-combo] Created smart-route: ' + c.models.length + ' models');
74
- }
75
- } catch (e) {}
76
- };
77
- setTimeout(sync, 5000);
78
- setInterval(sync, INTERVAL);`;
20
+ function build9RouterSmartRouteSyncScript() {
21
+ const lines = [
22
+ "const fs = require('fs');",
23
+ "const INTERVAL = 30000;",
24
+ "const DB_PATH = '/root/.9router/db/data.sqlite';",
25
+ "const PORT = process.env.PORT || 20128;",
26
+ "const COMBO_NAME = 'smart-route';",
27
+ "const API_BASE = `http://localhost:${PORT}`;",
28
+ "",
29
+ "function ensureSettings() {",
30
+ " try {",
31
+ " let db = null;",
32
+ " try {",
33
+ " const { DatabaseSync } = require('node:sqlite');",
34
+ " db = new DatabaseSync(DB_PATH);",
35
+ " } catch {",
36
+ " let Database;",
37
+ " try { Database = require('/usr/local/lib/node_modules/better-sqlite3'); } catch {",
38
+ " try { Database = require('better-sqlite3'); } catch { return; }",
39
+ " }",
40
+ " db = Database(DB_PATH);",
41
+ " }",
42
+ ' const existing = db.prepare("SELECT * FROM settings WHERE id = 1").get();',
43
+ " if (!existing) {",
44
+ ' db.prepare("INSERT INTO settings (id, data) VALUES (1, ?)").run(JSON.stringify({ requireLogin: false }));',
45
+ " } else {",
46
+ " try {",
47
+ " const data = JSON.parse(existing.data || '{}');",
48
+ " if (data.requireLogin !== false) {",
49
+ " data.requireLogin = false;",
50
+ ' db.prepare("UPDATE settings SET data = ? WHERE id = 1").run(JSON.stringify(data));',
51
+ " }",
52
+ " } catch {}",
53
+ " }",
54
+ " db.close();",
55
+ " } catch (e) {}",
56
+ "}",
57
+ "",
58
+ "const sync = async () => {",
59
+ " try {",
60
+ " if (!fs.existsSync(DB_PATH)) return;",
61
+ "",
62
+ " let existingCombo = null;",
63
+ " try {",
64
+ " const resp = await fetch(`${API_BASE}/api/combos`);",
65
+ " if (resp.status === 401) {",
66
+ " ensureSettings();",
67
+ " return;",
68
+ " }",
69
+ " const data = await resp.json();",
70
+ " if (data.combos) {",
71
+ " existingCombo = data.combos.find(c => c.name === COMBO_NAME);",
72
+ " }",
73
+ " } catch (e) { return; }",
74
+ "",
75
+ " if (existingCombo) return;",
76
+ "",
77
+ " let activeProviders = [];",
78
+ " try {",
79
+ " const resp = await fetch(`${API_BASE}/api/providers`);",
80
+ " const data = await resp.json();",
81
+ " const conns = data.connections || data.providerConnections || [];",
82
+ " activeProviders = [...new Set(",
83
+ " conns.filter(c => c && c.provider && c.isActive !== false && !c.disabled).map(c => c.provider)",
84
+ " )];",
85
+ " } catch (e) { return; }",
86
+ "",
87
+ " if (!activeProviders.length) return;",
88
+ "",
89
+ " let models = [];",
90
+ " try {",
91
+ " const resp = await fetch(`${API_BASE}/api/models`);",
92
+ " const data = await resp.json();",
93
+ " if (data.models && Array.isArray(data.models)) {",
94
+ " models = data.models",
95
+ " .filter(m => activeProviders.includes(m.provider))",
96
+ " .filter(m => !/(embedding|image|tts|stt|audio|vision)/i.test(m.model))",
97
+ " .map(m => m.fullModel);",
98
+ " }",
99
+ " models = [...new Set(models)];",
100
+ " } catch (e) { return; }",
101
+ "",
102
+ " if (!models.length) return;",
103
+ "",
104
+ " try {",
105
+ " await fetch(`${API_BASE}/api/combos`, {",
106
+ " method: 'POST',",
107
+ " headers: { 'Content-Type': 'application/json' },",
108
+ " body: JSON.stringify({ name: COMBO_NAME, models })",
109
+ " });",
110
+ " console.log('[sync-combo] Created smart-route with ' + models.length + ' models');",
111
+ " } catch (e) {}",
112
+ " } catch (e) {}",
113
+ "};",
114
+ "",
115
+ "if (fs.existsSync(DB_PATH)) ensureSettings();",
116
+ "setTimeout(sync, 10000);",
117
+ "setInterval(sync, INTERVAL);",
118
+ ];
119
+ return lines.join('\n');
79
120
  }
80
121
 
81
122
  function build9RouterPatchScript() {
@@ -140,20 +181,20 @@ for(const root of roots){if(!root||!fs.existsSync(root))continue;touched+=patchP
140
181
  if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}else{console.log('[patch-9router] No compatible 9router source files found to patch.');}`;
141
182
  }
142
183
 
143
- function build9RouterComposeEntrypointScript(syncScriptBase64, patchScriptBase64) {
184
+ function build9RouterComposeEntrypointScript(routerPort) {
185
+ const port = routerPort || 20128;
144
186
  const nineRouterSpec = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon && globalThis.__openclawCommon.NINE_ROUTER_NPM_SPEC) || '9router@latest';
145
187
  return [
146
- `npm install -g ${nineRouterSpec}`,
147
- `node -e "require('fs').writeFileSync('/tmp/patch-9router.js',Buffer.from('${patchScriptBase64}','base64').toString())"`,
148
- `node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
188
+ `npm install -g ` + nineRouterSpec + ` better-sqlite3`,
149
189
  'node /tmp/patch-9router.js || true',
190
+ 'node -e "const fs=require(\'fs\'),path=require(\'path\'); const DB_PATH=\'/root/.9router/db/data.sqlite\'; const dir=path.dirname(DB_PATH); if(!fs.existsSync(dir))fs.mkdirSync(dir,{recursive:true}); try{ const {DatabaseSync}=require(\'node:sqlite\'); const db=new DatabaseSync(DB_PATH); db.prepare(\'CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL)\').run(); const existing=db.prepare(\'SELECT * FROM settings WHERE id = 1\').get(); if(!existing){ db.prepare(\'INSERT INTO settings (id, data) VALUES (1, ?)\').run(JSON.stringify({requireLogin:false})); } db.close(); }catch(e){}" || true',
150
191
  'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
151
- 'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
192
+ `exec 9router -n -l -H 0.0.0.0 -p ${port} --skip-update`
152
193
  ].join('\n');
153
194
  }
154
195
 
155
196
  function buildGatewayPatchCmd() {
156
- return `node -e \\"const fs=require('fs'),os=require('os'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\"`;
197
+ return `node -e \\"const fs=require('fs'),os=require('os'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const gp=Number(process.env.OPENCLAW_GATEWAY_PORT||process.env.OPENCLAW_PORT)||c.gateway?.port||18789;const a=new Set(['http://localhost:'+gp,'http://127.0.0.1:'+gp,'http://0.0.0.0:'+gp]);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':'+gp);}}const p9=c.models&&c.models.providers&&c.models.providers['9router'];if(p9){p9.request=Object.assign({},p9.request,{allowPrivateNetwork:true});}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:gp,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\"`;
157
198
  }
158
199
 
159
200
  function buildDockerArtifacts(options) {
@@ -163,14 +204,13 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
163
204
  is9Router,
164
205
  isLocal,
165
206
  isMultiBot,
166
- hasBrowser,
167
207
  selectedModel,
168
208
  agentId,
169
209
  allSkills = [],
170
210
  dockerfilePlugins = [],
171
211
  dockerfileSkillInstallMode = 'none',
172
212
  runtimeCommandParts = [],
173
- volumeMount = '../../.openclaw:/root/project/.openclaw\\n - ../../:/mnt/project',
213
+ volumeMount = '../../.openclaw:/root/project/.openclaw\n - ../../:/mnt/project',
174
214
  singleComposeName = 'oc-bot',
175
215
  multiComposeName = 'oc-multibot',
176
216
  singleAppContainerName = 'openclaw-bot',
@@ -182,22 +222,9 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
182
222
  plainSingleExtraHosts = false,
183
223
  multiOllamaNumParallel = 1,
184
224
  singleOllamaNumParallel = 1,
185
- emitBrowserInstall = true,
186
-
225
+ gatewayPort = 18789,
226
+ routerPort = 20128,
187
227
  } = options;
188
-
189
- const browserAptExtra = hasBrowser ? ' xvfb socat' : '';
190
- const browserInstallLines = hasBrowser && emitBrowserInstall
191
- ? [
192
- '',
193
- '# Browser Automation: Playwright engine (needed for native CDP)',
194
- 'RUN npm install -g agent-browser playwright \\',
195
- ' && npx playwright install chromium --with-deps \\',
196
- ' && ln -f -s /root/.cache/ms-playwright/chromium-*/chrome-linux*/chrome /usr/bin/google-chrome',
197
- '',
198
- ''
199
- ].join('\n')
200
- : '';
201
228
  const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
202
229
  ? `\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`
203
230
  : '';
@@ -209,10 +236,9 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
209
236
  // Dynamic runtime configuration: backup config before any first-run install, restore after.
210
237
  // Missing plugin install may touch openclaw.json, so preserve critical fields.
211
238
  const backupConfigScript = `const fs=require('fs'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json'),b=p.replace('openclaw.json','.openclaw-config-backup.json');if(fs.existsSync(p)){fs.copyFileSync(p,b);}`;
212
- const backupConfigB64 = encodeBase64Utf8(backupConfigScript);
213
239
 
214
- const restoreConfigScript = `const fs=require('fs'),os=require('os'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json'),b=p.replace('openclaw.json','.openclaw-config-backup.json');if(fs.existsSync(p)&&fs.existsSync(b)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const bk=JSON.parse(fs.readFileSync(b,'utf8'));const keep=['agents','channels','bindings','commands','models','browser','skills'];for(const k of keep){if(bk[k]&&!c[k])c[k]=bk[k];}const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://'+entry.address+':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',mode:c.gateway?.mode||bk.gateway?.mode||'local',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));fs.unlinkSync(b);}`;
215
- const restoreConfigB64 = encodeBase64Utf8(restoreConfigScript);
240
+ const restoreConfigScript = `const fs=require('fs'),os=require('os'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json'),b=p.replace('openclaw.json','.openclaw-config-backup.json');if(fs.existsSync(p)&&fs.existsSync(b)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const bk=JSON.parse(fs.readFileSync(b,'utf8'));const keep=['agents','channels','bindings','commands','models','browser','skills','plugins','tools'];for(const k of keep){if(bk[k]&&!c[k])c[k]=bk[k];}const gp=Number(process.env.OPENCLAW_GATEWAY_PORT||process.env.OPENCLAW_PORT)||c.gateway?.port||bk.gateway?.port||18789;const a=new Set(['http://localhost:'+gp,'http://127.0.0.1:'+gp,'http://0.0.0.0:'+gp]);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://'+entry.address+':'+gp);}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:gp,bind:'custom',customBindHost:'0.0.0.0',mode:c.gateway?.mode||bk.gateway?.mode||'local',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));fs.unlinkSync(b);}`;
241
+ const securityCompatScript = `const fs=require('fs'),path=require('path');const scopes=['operator.admin','operator.pairing','operator.approvals'];function uniq(a){return Array.from(new Set([...(Array.isArray(a)?a:[]),...scopes]));}function walk(v){if(!v||typeof v!=='object')return;if(Array.isArray(v)){v.forEach(walk);return;}if(Array.isArray(v.scopes)||Array.isArray(v.approvedScopes)){v.scopes=uniq(v.scopes);v.approvedScopes=uniq(v.approvedScopes);}Object.values(v).forEach(walk);}const home=process.env.OPENCLAW_HOME||path.join(process.cwd(),'.openclaw');const state=process.env.OPENCLAW_STATE_DIR||home;const cfgPath=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(cfgPath)){const c=JSON.parse(fs.readFileSync(cfgPath,'utf8'));const p=c.models&&c.models.providers&&c.models.providers['9router'];if(p){p.request=Object.assign({},p.request,{allowPrivateNetwork:true});}fs.writeFileSync(cfgPath,JSON.stringify(c,null,2));}for(const root of Array.from(new Set([home,state]))){const f=path.join(root,'devices','paired.json');if(fs.existsSync(f)){const d=JSON.parse(fs.readFileSync(f,'utf8'));walk(d);fs.writeFileSync(f,JSON.stringify(d,null,2));}}`;
216
242
 
217
243
  const runtimeParts = runtimeCommandParts.filter(Boolean);
218
244
  const runtimePrelude = [
@@ -238,6 +264,20 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
238
264
  ' echo "[entrypoint] plugin $id missing; installing $spec"',
239
265
  ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
240
266
  '}',
267
+ 'ensure_zalouser() {',
268
+ ' NPM_DIR="$OPENCLAW_HOME/npm"',
269
+ ' PKG_DIR="$NPM_DIR/node_modules/@openclaw/zalouser"',
270
+ ' if [ -d "$PKG_DIR" ]; then',
271
+ ' echo "[entrypoint] zalouser plugin already installed"',
272
+ ' else',
273
+ ' echo "[entrypoint] zalouser plugin missing; installing via npm"',
274
+ ' mkdir -p "$NPM_DIR"',
275
+ ' cd "$NPM_DIR"',
276
+ ' npm init -y 2>/dev/null || true',
277
+ ' npm install @openclaw/zalouser@latest 2>/dev/null || echo "[entrypoint] warning: failed to install @openclaw/zalouser"',
278
+ ' cd /root/project',
279
+ ' fi',
280
+ '}',
241
281
  'ensure_skill() {',
242
282
  ' id="$1"',
243
283
  ' if find "$OPENCLAW_HOME" -maxdepth 4 -type d -path "*/skills/$id" -print -quit 2>/dev/null | grep -q .; then',
@@ -251,40 +291,59 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
251
291
  ];
252
292
  runtimeParts.unshift(...runtimePrelude);
253
293
  // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
254
- runtimeParts.unshift(`node -e 'eval(Buffer.from("${backupConfigB64}","base64").toString())'`);
294
+ runtimeParts.unshift(`node - <<'NODE'\n${backupConfigScript}\nNODE`);
255
295
  // Restore config AFTER plugin installs (which may clobber openclaw.json)
256
- runtimeParts.push(`node -e 'eval(Buffer.from("${restoreConfigB64}","base64").toString())'`);
257
- if (hasBrowser) {
258
- runtimeParts.push('socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 &');
259
- runtimeParts.push('Xvfb :99 -screen 0 1280x720x24 > /dev/null 2>&1 & DISPLAY=:99 openclaw gateway run');
260
- } else {
261
- runtimeParts.push('openclaw gateway run');
262
- }
296
+ runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
297
+ runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
298
+ // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
299
+ runtimeParts.push([
300
+ '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
301
+ 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
302
+ 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
303
+ ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
304
+ ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
305
+ 'fi',
306
+ ].join('\n'));
307
+ runtimeParts.push([
308
+ '# Zalo channel auto-restart monitor (background)',
309
+ '(',
310
+ ' sleep 180',
311
+ ' while true; do',
312
+ ' sleep 60',
313
+ ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
314
+ ' if echo "$STATUS" | grep -qi "stopped"; then',
315
+ ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
316
+ ' sleep 5',
317
+ ' kill 1 2>/dev/null || true',
318
+ ' fi',
319
+ ' done',
320
+ ') &',
321
+ ].join('\n'));
322
+ runtimeParts.push('openclaw gateway run');
263
323
  const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
264
- const runtimeScriptB64 = encodeBase64Utf8(runtimeScript);
265
324
  const dockerfile = `FROM node:22-slim
266
325
 
267
- RUN apt-get update && apt-get install -y git curl python3${browserAptExtra} && rm -rf /var/lib/apt/lists/*
268
- ${browserInstallLines}
326
+ RUN apt-get update && apt-get install -y git curl python3 && rm -rf /var/lib/apt/lists/*
327
+
269
328
  ARG OPENCLAW_VER="${openClawNpmSpec}"
270
329
  ARG CACHE_BUST=""
271
330
  RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
272
331
  ${patchLine}
273
- RUN node -e "require('fs').writeFileSync('/usr/local/bin/openclaw-entrypoint.sh', Buffer.from('${runtimeScriptB64}','base64').toString())" && chmod +x /usr/local/bin/openclaw-entrypoint.sh
332
+
333
+ COPY entrypoint.sh /usr/local/bin/openclaw-entrypoint.sh
334
+ RUN chmod +x /usr/local/bin/openclaw-entrypoint.sh
274
335
  WORKDIR /root/project
275
336
 
276
- EXPOSE 18791
337
+ EXPOSE ${gatewayPort}
277
338
 
278
339
  CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
279
340
 
280
- const syncScript = build9RouterSmartRouteSyncScript('/root/.9router/db.json');
281
- const syncScriptBase64 = encodeBase64Utf8(syncScript);
282
- const patchScript = build9RouterPatchScript();
283
- const patchScriptBase64 = encodeBase64Utf8(patchScript);
284
- const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncScriptBase64, patchScriptBase64);
341
+ const syncScript = build9RouterSmartRouteSyncScript();
342
+ const patchScript = build9RouterPatchScript();
343
+ const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
285
344
  const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
286
345
 
287
- const appEnvironmentBlock = ' environment:\n - OPENCLAW_HOME=/root/project/.openclaw\n - OPENCLAW_STATE_DIR=/root/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n tmpfs:\n - /root/project/.openclaw/plugin-runtime-deps\n';
346
+ const appEnvironmentBlock = ` environment:\n - OPENCLAW_HOME=/root/project/.openclaw\n - OPENCLAW_STATE_DIR=/root/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n - OPENCLAW_GATEWAY_PORT=${gatewayPort}\n - OPENCLAW_PORT=${gatewayPort}\n tmpfs:\n - /root/project/.openclaw/plugin-runtime-deps\n`;
288
347
 
289
348
  let compose;
290
349
  if (isMultiBot) {
@@ -293,7 +352,7 @@ const patchScript = build9RouterPatchScript();
293
352
  : isLocal
294
353
  ? ' depends_on:\n ollama:\n condition: service_healthy\n'
295
354
  : '';
296
- const extraHosts = hasBrowser ? `${extraHostsBlock}\n` : '';
355
+ const extraHosts = `${extraHostsBlock}\n`;
297
356
  if (is9Router) {
298
357
  compose = `name: ${multiComposeName}
299
358
  services:
@@ -302,11 +361,11 @@ services:
302
361
  container_name: ${multiAppContainerName}
303
362
  restart: always
304
363
  env_file:
305
- - .env
364
+ - ../../.env
306
365
  ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
307
366
  - ${volumeMount}
308
367
  ports:
309
- - "18791:18791"
368
+ - "${gatewayPort}:${gatewayPort}"
310
369
 
311
370
  9router:
312
371
  image: node:22-slim
@@ -318,13 +377,15 @@ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
318
377
  - |
319
378
  ${indentBlock(docker9RouterEntrypointScript, 8)}
320
379
  environment:
321
- - PORT=20128
380
+ - PORT=${routerPort}
322
381
  - HOSTNAME=0.0.0.0
323
382
  - CI=true
324
383
  volumes:
325
384
  - 9router-data:/root/.9router
385
+ - ./sync.js:/tmp/sync.js:ro
386
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
326
387
  ports:
327
- - "20128:20128"
388
+ - "${routerPort}:${routerPort}"
328
389
 
329
390
  volumes:
330
391
  9router-data:`;
@@ -337,11 +398,11 @@ services:
337
398
  container_name: ${multiAppContainerName}
338
399
  restart: always
339
400
  env_file:
340
- - .env
401
+ - ../../.env
341
402
  ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
342
403
  - ${volumeMount}
343
404
  ports:
344
- - "18791:18791"
405
+ - "${gatewayPort}:${gatewayPort}"
345
406
 
346
407
  ollama:
347
408
  image: ollama/ollama:latest
@@ -377,11 +438,11 @@ services:
377
438
  container_name: ${multiAppContainerName}
378
439
  restart: always
379
440
  env_file:
380
- - .env
441
+ - ../../.env
381
442
  ${appEnvironmentBlock}${extraHosts} volumes:
382
443
  - ${volumeMount}
383
444
  ports:
384
- - "18791:18791"`;
445
+ - "${gatewayPort}:${gatewayPort}"`;
385
446
  }
386
447
  } else if (is9Router) {
387
448
  compose = `name: ${singleComposeName}
@@ -391,13 +452,14 @@ services:
391
452
  container_name: ${singleAppContainerName}
392
453
  restart: always
393
454
  env_file:
394
- - .env
455
+ - ../../.env
395
456
  depends_on:
396
457
  - 9router
397
- ${appEnvironmentBlock}${hasBrowser ? `${extraHostsBlock}\n` : ''} volumes:
458
+ ${appEnvironmentBlock}${extraHostsBlock}\n volumes:
398
459
  - ${volumeMount}
460
+ - openclaw-plugins:/root/project/.openclaw/npm
399
461
  ports:
400
- - "18791:18791"
462
+ - "${gatewayPort}:${gatewayPort}"
401
463
 
402
464
  9router:
403
465
  image: node:22-slim
@@ -409,16 +471,19 @@ ${appEnvironmentBlock}${hasBrowser ? `${extraHostsBlock}\n` : ''} volumes:
409
471
  - |
410
472
  ${indentBlock(docker9RouterEntrypointScript, 8)}
411
473
  environment:
412
- - PORT=20128
474
+ - PORT=${routerPort}
413
475
  - HOSTNAME=0.0.0.0
414
476
  - CI=true
415
477
  volumes:
416
478
  - 9router-data:/root/.9router
479
+ - ./sync.js:/tmp/sync.js:ro
480
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
417
481
  ports:
418
- - "20128:20128"
482
+ - "${routerPort}:${routerPort}"
419
483
 
420
484
  volumes:
421
- 9router-data:`;
485
+ 9router-data:
486
+ openclaw-plugins:`;
422
487
  } else if (isLocal) {
423
488
  const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
424
489
  compose = `name: ${singleComposeName}
@@ -427,12 +492,12 @@ services:
427
492
  build: .
428
493
  container_name: ${singleAppContainerName}
429
494
  restart: always
430
- env_file: .env
495
+ env_file: ../../.env
431
496
  ${appEnvironmentBlock} depends_on:
432
497
  ollama:
433
498
  condition: service_healthy
434
- ${hasBrowser ? `${extraHostsBlock}\n` : ''} ports:
435
- - "18791:18791"
499
+ ${extraHostsBlock}\n ports:
500
+ - "${gatewayPort}:${gatewayPort}"
436
501
  volumes:
437
502
  - ${volumeMount}
438
503
 
@@ -470,17 +535,19 @@ services:
470
535
  container_name: ${singleAppContainerName}
471
536
  restart: always
472
537
  env_file:
473
- - .env
538
+ - ../../.env
474
539
  ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
475
540
  - ${volumeMount}
476
541
  ports:
477
- - "18791:18791"`;
542
+ - "${gatewayPort}:${gatewayPort}"`;
478
543
  }
479
544
 
480
545
  return {
481
546
  dockerfile,
482
547
  compose,
548
+ entrypointScript: runtimeScript,
483
549
  syncScript,
550
+ patchScript,
484
551
  docker9RouterEntrypointScript,
485
552
  gatewayPatchCmd: buildGatewayPatchCmd(),
486
553
  };
@@ -500,3 +567,5 @@ ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''}
500
567
  if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
501
568
  Object.assign(exports, globalThis.__openclawDockerGen);
502
569
  }
570
+
571
+