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,586 +1,606 @@
1
- // @ts-nocheck
2
- (function (root) {
3
- const common = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon) || {};
4
- const SUPPORTED_CODEX_MODELS = common.SUPPORTED_CODEX_MODELS || ['cx/gpt-5.4', 'cx/gpt-5.3-codex', 'cx/gpt-5.2', 'cx/gpt-5.4-mini'];
5
- const SMART_ROUTE_PROVIDER_MODELS = common.SMART_ROUTE_PROVIDER_MODELS || { codex: SUPPORTED_CODEX_MODELS };
6
- const SMART_ROUTE_PROVIDER_ORDER = common.SMART_ROUTE_PROVIDER_ORDER || ['codex'];
7
-
8
- function encodeBase64Utf8(value) {
9
- if (typeof Buffer !== 'undefined') {
10
- return Buffer.from(String(value), 'utf8').toString('base64');
11
- }
12
- return btoa(String.fromCharCode(...new TextEncoder().encode(String(value))));
13
- }
14
-
15
- function indentBlock(text, spaces) {
16
- const prefix = ' '.repeat(spaces);
17
- return String(text).split('\n').map((line) => `${prefix}${line}`).join('\n');
18
- }
19
-
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');
120
- }
121
-
122
- function build9RouterPatchScript() {
123
- return `const fs=require('fs');const path=require('path');const cp=require('child_process');
124
- const MODELS=${JSON.stringify(SUPPORTED_CODEX_MODELS.map((model) => model.replace('cx/', '')))};
125
- const MODEL_NAMES={"gpt-5.4":"GPT 5.4","gpt-5.4-mini":"GPT 5.4 Mini","gpt-5.3-codex":"GPT 5.3 Codex","gpt-5.2":"GPT 5.2"};
126
- const SELF_TEST_BLOCK=[
127
- 'codex: {',
128
- ' url: "https://chatgpt.com/backend-api/codex/responses",',
129
- ' method: "POST",',
130
- ' authHeader: "Authorization",',
131
- ' authPrefix: "Bearer ",',
132
- ' extraHeaders: { "Content-Type": "application/json", "originator": "codex-cli", "User-Agent": "codex-cli/1.0.18 (macOS; arm64)" },',
133
- ' body: JSON.stringify({',
134
- ' model: "gpt-5.2",',
135
- ' instructions: "You are a coding assistant.",',
136
- ' input: [{ role: "user", content: [{ type: "input_text", text: "Reply with exactly: ok" }] }],',
137
- ' stream: true,',
138
- ' store: false,',
139
- ' }),',
140
- ' acceptStatuses: [200, 400],',
141
- ' refreshable: true,',
142
- ' },'
143
- ].join('\\n');
144
- const roots=new Set();
145
- function add(p){if(p)roots.add(p);}
146
- try{const npmRoot=cp.execSync('npm root -g',{stdio:['ignore','pipe','ignore'],encoding:'utf8'}).trim();if(npmRoot)add(path.join(npmRoot,'9router'));}catch{}
147
- add(path.join(process.env.APPDATA||'','npm','node_modules','9router'));
148
- add('/usr/local/lib/node_modules/9router');
149
- add('/usr/lib/node_modules/9router');
150
- add(path.join(process.cwd(),'node_modules','9router'));
151
- function patchFile(filePath, transform){if(!fs.existsSync(filePath))return false;const before=fs.readFileSync(filePath,'utf8');const after=transform(before);if(!after||after===before)return false;fs.writeFileSync(filePath,after);return true;}
152
- function patchText(text,replacers){let next=text;for(const replacer of replacers){next=replacer(next);}return next===text?null:next;}
153
- function patchProviderModels(root){return patchFile(path.join(root,'open-sse','config','providerModels.js'),(text)=>text.replace(/cx:\\s*\\[[\\s\\S]*?\\],/,()=>{const lines=MODELS.map((id)=>' { id: "'+id+'", name: "'+(MODEL_NAMES[id]||id)+'" },');return 'cx: [ // OpenAI Codex\\n'+lines.join('\\n')+'\\n ],';}));}
154
- function patchCodexLikeFile(filePath){return patchFile(filePath,(text)=>{if(text.includes('max_output_tokens'))return text;return patchText(text,[
155
- (value)=>value.replace(/delete (\\w+)\\.max_tokens,delete \\1\\.user/g,'delete $1.max_tokens,delete $1.max_output_tokens,delete $1.user'),
156
- (value)=>value.replace(/delete (\\w+)\\.max_tokens;(\\s*)delete \\1\\.user/g,'delete $1.max_tokens;$2delete $1.max_output_tokens;$2delete $1.user'),
157
- (value)=>value.replace(' delete body.max_tokens;\\n',' delete body.max_tokens;\\n delete body.max_output_tokens;\\n')
158
- ]);});}
159
- function patchCodexExecutor(root){let touched=0;touched+=patchCodexLikeFile(path.join(root,'open-sse','executors','codex.js'))?1:0;const chunksDir=path.join(root,'app','.next','server','chunks');if(fs.existsSync(chunksDir)){for(const entry of fs.readdirSync(chunksDir)){if(!entry.endsWith('.js'))continue;touched+=patchCodexLikeFile(path.join(chunksDir,entry))?1:0;}}return touched;}
160
- function patchResponsesNullGuard(root){let touched=0;const chunksDir=path.join(root,'app','.next','server','chunks');if(!fs.existsSync(chunksDir))return touched;for(const entry of fs.readdirSync(chunksDir)){if(!entry.endsWith('.js'))continue;touched+=patchFile(path.join(chunksDir,entry),(text)=>patchText(text,[
161
- (value)=>value.replace('let b=a.content.find(a=>"output_text"===a.type);','let b=a.content.find(a=>a&&"output_text"===a.type);'),
162
- (value)=>value.replace('let c=a.content.find(a=>"string"==typeof a.text);','let c=a.content.find(a=>a&&"string"==typeof a.text);'),
163
- (value)=>value.replace('let b=a.filter(a=>a?.type==="message");','let b=a.filter(a=>a&&a?.type==="message");'),
164
- (value)=>value.replace('for(let a of j){let b=a.type||(a.role?"message":null);','for(let a of j){let b=a&&(a.type||(a.role?"message":null));'),
165
- (value)=>value.replace('for(let a of b.messages||[]){if("system"===a.role){','for(let a of b.messages||[])if(a){if("system"===a.role){'),
166
- (value)=>value.replace('let b=Array.isArray(a.content)?a.content.map(a=>"input_text"===a.type||"output_text"===a.type?{type:"text",text:a.text}:"input_image"===a.type?{type:"image_url",image_url:{url:a.image_url||a.file_id||"",detail:a.detail||"auto"}}:a):a.content;','let b=Array.isArray(a.content)?a.content.map(a=>a&&("input_text"===a.type||"output_text"===a.type)?{type:"text",text:a.text}:a&&"input_image"===a.type?{type:"image_url",image_url:{url:a.image_url||a.file_id||"",detail:a.detail||"auto"}}:a).filter(Boolean):a.content;'),
167
- (value)=>value.replace('c="string"==typeof a.content?[{type:b,text:a.content}]:Array.isArray(a.content)?a.content.map(a=>{if("text"===a.type)return{type:b,text:a.text};if("image_url"===a.type)return{type:"input_image",image_url:"string"==typeof a.image_url?a.image_url:a.image_url?.url,detail:a.image_url?.detail||"auto"};if("input_image"===a.type)return a;let c=a.text||a.content||JSON.stringify(a);return{type:b,text:"string"==typeof c?c:JSON.stringify(c)}}):[];','c="string"==typeof a.content?[{type:b,text:a.content}]:Array.isArray(a.content)?a.content.map(a=>{if(!a)return null;if("text"===a.type)return{type:b,text:a.text};if("image_url"===a.type)return{type:"input_image",image_url:"string"==typeof a.image_url?a.image_url:a.image_url?.url,detail:a.image_url?.detail||"auto"};if("input_image"===a.type)return a;let c=a.text||a.content||JSON.stringify(a);return{type:b,text:"string"==typeof c?c:JSON.stringify(c)}}).filter(Boolean):[];'),
168
- (value)=>value.replace('b.tools&&Array.isArray(b.tools)&&(e.tools=b.tools.map(a=>{if(a.function)return a;let b=a.name;return b&&"string"==typeof b&&""!==b.trim()?{type:"function",function:{name:b,description:String(a.description||""),parameters:i(a.parameters),strict:a.strict}}:null}).filter(Boolean))','b.tools&&Array.isArray(b.tools)&&(e.tools=b.tools.map(a=>{if(!a)return null;if(a.function)return a;let b=a.name;return b&&"string"==typeof b&&""!==b.trim()?{type:"function",function:{name:b,description:String(a.description||""),parameters:i(a.parameters),strict:a.strict}}:null}).filter(Boolean))'),
169
- (value)=>value.replace('b.tools&&Array.isArray(b.tools)&&(e.tools=b.tools.map(a=>"function"===a.type?{type:"function",name:a.function.name,description:String(a.function.description||""),parameters:i(a.function.parameters),strict:a.function.strict}:a)),','b.tools&&Array.isArray(b.tools)&&(e.tools=b.tools.map(a=>a&&"function"===a.type?{type:"function",name:a.function.name,description:String(a.function.description||""),parameters:i(a.function.parameters),strict:a.function.strict}:a).filter(Boolean)),'),
170
- (value)=>value.replace('filter(a=>"function_call"===a.type)','filter(a=>a&&"function_call"===a.type)'),
171
- (value)=>value.replace(/filter\\(a=>"text"===a\\.type\\)/g,'filter(a=>a&&"text"===a.type)'),
172
- (value)=>value.replace(/find\\(a=>"message_stop"===a\\.type\\)/g,'find(a=>a&&"message_stop"===a.type)'),
173
- (value)=>value.replace(/find\\(a=>"content_block_delta"===a\\.type\\)/g,'find(a=>a&&"content_block_delta"===a.type)'),
174
- (value)=>value.replace(/find\\(a=>"message_delta"===a\\.type\\)/g,'find(a=>a&&"message_delta"===a.type)'),
175
- (value)=>value.replace(/find\\(a=>"message_start"===a\\.type\\)/g,'find(a=>a&&"message_start"===a.type)'),
176
- (value)=>value.replace(/for\\(let e of a\\.content\\)(?!if\\(e\\))/g,'for(let e of a.content)if(e)')
177
- ] ))?1:0;}return touched;}
178
- function patchSelfTest(root){return patchFile(path.join(root,'src','app','api','providers','[id]','test','testUtils.js'),(text)=>{if(text.includes('model: "gpt-5.2"')&&text.includes('store: false')&&text.includes('acceptStatuses: [200, 400]'))return text;return text.replace(/codex:\\s*\\{[\\s\\S]*?refreshable:\\s*true,\\s*\\},/,SELF_TEST_BLOCK);});}
179
- let touched=0;
180
- for(const root of roots){if(!root||!fs.existsSync(root))continue;touched+=patchProviderModels(root)?1:0;touched+=patchCodexExecutor(root)?1:0;touched+=patchResponsesNullGuard(root)?1:0;touched+=patchSelfTest(root)?1:0;}
181
- if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}else{console.log('[patch-9router] No compatible 9router source files found to patch.');}`;
182
- }
183
-
184
- function build9RouterComposeEntrypointScript(routerPort) {
185
- const port = routerPort || 20128;
186
- const nineRouterSpec = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon && globalThis.__openclawCommon.NINE_ROUTER_NPM_SPEC) || '9router@latest';
187
- return [
188
- `npm install -g ` + nineRouterSpec,
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',
191
- 'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
192
- `exec 9router -n -l -H 0.0.0.0 -p ${port} --skip-update`
193
- ].join('\n');
194
- }
195
-
196
- function buildGatewayPatchCmd() {
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));}\\"`;
198
- }
199
-
200
- function buildDockerArtifacts(options) {
201
- const {
202
- openClawNpmSpec,
203
- openClawRuntimePackages,
204
- is9Router,
205
- isLocal,
206
- isMultiBot,
207
- hasBrowser = false,
208
- selectedModel,
209
- agentId,
210
- allSkills = [],
211
- dockerfilePlugins = [],
212
- dockerfileSkillInstallMode = 'none',
213
- runtimeCommandParts = [],
214
- volumeMount = '../../.openclaw:/home/node/project/.openclaw\n - ../../:/mnt/project',
215
- singleComposeName = 'oc-bot',
216
- multiComposeName = 'oc-multibot',
217
- singleAppContainerName = 'openclaw-bot',
218
- multiAppContainerName = 'openclaw-multibot',
219
- singleRouterContainerName = '9router',
220
- multiRouterContainerName = '9router-multibot',
221
- singleOllamaContainerName = 'ollama',
222
- multiOllamaContainerName = 'ollama-multibot',
223
- plainSingleExtraHosts = false,
224
- multiOllamaNumParallel = 1,
225
- singleOllamaNumParallel = 1,
226
- gatewayPort = 18789,
227
- routerPort = 20128,
228
- osChoice = '',
229
- } = options;
230
- const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
231
- ? `\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`
232
- : '';
233
- const pluginLines = dockerfilePlugins.length > 0
234
- ? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
235
- : '';
236
- const patchLine = `RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"`;
237
-
238
- // Dynamic runtime configuration: backup config before any first-run install, restore after.
239
- // Missing plugin install may touch openclaw.json, so preserve critical fields.
240
- 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);}`;
241
-
242
- 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);}`;
243
- 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));}}`;
244
-
245
- const runtimeParts = runtimeCommandParts.filter(Boolean);
246
- const runtimePrelude = [
247
- 'export OPENCLAW_HOME="${OPENCLAW_HOME:-$PWD/.openclaw}"',
248
- 'export OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"',
249
- 'mkdir -p "$OPENCLAW_HOME" "$OPENCLAW_STATE_DIR"',
250
- 'if [ "$OPENCLAW_STATE_DIR" != "$OPENCLAW_HOME" ]; then',
251
- ' for path in "$OPENCLAW_HOME"/*; do',
252
- ' [ -e "$path" ] || continue',
253
- ' name="$(basename "$path")"',
254
- ' [ "$name" = "plugin-runtime-deps" ] && continue',
255
- ' [ "$name" = "logs" ] && continue',
256
- ' [ -e "$OPENCLAW_STATE_DIR/$name" ] || ln -s "$path" "$OPENCLAW_STATE_DIR/$name"',
257
- ' done',
258
- 'fi',
259
- 'ensure_plugin() {',
260
- ' id="$1"',
261
- ' spec="$2"',
262
- ' if [ -d "$OPENCLAW_HOME/extensions/$id" ]; then',
263
- ' echo "[entrypoint] plugin $id already installed"',
264
- ' return 0',
265
- ' fi',
266
- ' echo "[entrypoint] plugin $id missing; installing $spec"',
267
- ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
268
- '}',
269
- 'ensure_zalouser() {',
270
- ' NPM_DIR="$OPENCLAW_HOME/npm"',
271
- ' PKG_DIR="$NPM_DIR/node_modules/@openclaw/zalouser"',
272
- ' if [ -d "$PKG_DIR" ]; then',
273
- ' echo "[entrypoint] zalouser plugin already installed"',
274
- ' else',
275
- ' echo "[entrypoint] zalouser plugin missing; installing via npm"',
276
- ' mkdir -p "$NPM_DIR"',
277
- ' cd "$NPM_DIR"',
278
- ' npm init -y 2>/dev/null || true',
279
- ' npm install @openclaw/zalouser@latest 2>/dev/null || echo "[entrypoint] warning: failed to install @openclaw/zalouser"',
280
- ' cd /home/node/project',
281
- ' fi',
282
- '}',
283
- 'ensure_skill() {',
284
- ' id="$1"',
285
- ' if find "$OPENCLAW_HOME" -maxdepth 4 -type d -path "*/skills/$id" -print -quit 2>/dev/null | grep -q .; then',
286
- ' echo "[entrypoint] skill $id already installed"',
287
- ' return 0',
288
- ' fi',
289
- ' echo "[entrypoint] skill $id missing; installing"',
290
- ' openclaw skills install "$id" 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
291
- '}',
292
- 'echo "[entrypoint] ensuring runtime assets, then starting gateway"',
293
- ];
294
- runtimeParts.unshift(...runtimePrelude);
295
- // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
296
- runtimeParts.unshift(`node - <<'NODE'\n${backupConfigScript}\nNODE`);
297
- // Restore config AFTER plugin installs (which may clobber openclaw.json)
298
- runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
299
- runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
300
- // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
301
- runtimeParts.push([
302
- '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
303
- 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
304
- 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
305
- ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
306
- ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
307
- 'fi',
308
- '# Automatically run Zalo sticker-mention patch if skill is present',
309
- 'for MENTIONS_JS in $(find "$OPENCLAW_HOME" -maxdepth 5 -path "*/skills/*/mentions.js" -type f 2>/dev/null); do',
310
- ' if [ -f "$MENTIONS_JS" ]; then',
311
- ' echo "[entrypoint] Running patch: $MENTIONS_JS"',
312
- ' node "$MENTIONS_JS" || echo "[entrypoint] Warning: failed to run patch $MENTIONS_JS"',
313
- ' fi',
314
- 'done',
315
- ].join('\n'));
316
- runtimeParts.push([
317
- '# Zalo channel auto-restart monitor (background)',
318
- '(',
319
- ' sleep 180',
320
- ' while true; do',
321
- ' sleep 60',
322
- ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
323
- ' if echo "$STATUS" | grep -qi "stopped"; then',
324
- ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
325
- ' sleep 5',
326
- ' kill 1 2>/dev/null || true',
327
- ' fi',
328
- ' done',
329
- ') &',
330
- ].join('\n'));
331
- runtimeParts.push('openclaw gateway run');
332
- const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
333
- let browserInstall = '';
334
- if (hasBrowser) {
335
- browserInstall = '\n# Install browser and system dependencies for Playwright\nRUN npx playwright install-deps chromium && npx playwright install chromium\n';
336
- }
337
- const dockerfile = `FROM node:22-slim
338
-
339
- RUN apt-get update && apt-get install -y git curl python3 && rm -rf /var/lib/apt/lists/*
340
-
341
- ARG OPENCLAW_VER="${openClawNpmSpec}"
342
- ARG CACHE_BUST=""
343
- RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
344
- ${patchLine}${browserInstall}
345
-
346
- COPY entrypoint.sh /usr/local/bin/openclaw-entrypoint.sh
347
- RUN chmod +x /usr/local/bin/openclaw-entrypoint.sh
348
- WORKDIR /home/node/project
349
-
350
- EXPOSE ${gatewayPort}
351
-
352
- CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
353
-
354
- const syncScript = build9RouterSmartRouteSyncScript();
355
- const patchScript = build9RouterPatchScript();
356
- const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
357
- const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
358
-
359
- const appEnvironmentBlock = ` environment:\n - HOME=/home/node/project/.openclaw\n - OPENCLAW_HOME=/home/node/project/.openclaw\n - OPENCLAW_STATE_DIR=/home/node/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n - OPENCLAW_SETUP_OS=${osChoice || ''}\n - OPENCLAW_BROWSER_HOST_OS=${osChoice || ''}\n - OPENCLAW_GATEWAY_PORT=${gatewayPort}\n - OPENCLAW_PORT=${gatewayPort}\n tmpfs:\n - /home/node/project/.openclaw/plugin-runtime-deps\n`;
360
-
361
- let compose;
362
- if (isMultiBot) {
363
- const dependsOn = is9Router
364
- ? ' depends_on:\n - 9router\n'
365
- : isLocal
366
- ? ' depends_on:\n ollama:\n condition: service_healthy\n'
367
- : '';
368
- const extraHosts = `${extraHostsBlock}\n`;
369
- if (is9Router) {
370
- compose = `name: ${multiComposeName}
371
- services:
372
- ai-bot:
373
- build: .
374
- container_name: ${multiAppContainerName}
375
- restart: always
376
- env_file:
377
- - ../../.env
378
- ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
379
- - ${volumeMount}
380
- ports:
381
- - "${gatewayPort}:${gatewayPort}"
382
-
383
- 9router:
384
- image: node:22-slim
385
- container_name: ${multiRouterContainerName}
386
- restart: always
387
- entrypoint:
388
- - /bin/sh
389
- - -c
390
- - |
391
- ${indentBlock(docker9RouterEntrypointScript, 8)}
392
- environment:
393
- - PORT=${routerPort}
394
- - HOSTNAME=0.0.0.0
395
- - CI=true
396
- volumes:
397
- - 9router-data:/root/.9router
398
- - ./sync.js:/tmp/sync.js:ro
399
- - ./patch-9router.js:/tmp/patch-9router.js:ro
400
- ports:
401
- - "${routerPort}:${routerPort}"
402
-
403
- volumes:
404
- 9router-data:`;
405
- } else if (isLocal) {
406
- const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
407
- compose = `name: ${multiComposeName}
408
- services:
409
- ai-bot:
410
- build: .
411
- container_name: ${multiAppContainerName}
412
- restart: always
413
- env_file:
414
- - ../../.env
415
- ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
416
- - ${volumeMount}
417
- ports:
418
- - "${gatewayPort}:${gatewayPort}"
419
-
420
- ollama:
421
- image: ollama/ollama:latest
422
- container_name: ${multiOllamaContainerName}
423
- restart: always
424
- environment:
425
- - OLLAMA_KEEP_ALIVE=24h
426
- - OLLAMA_NUM_PARALLEL=${multiOllamaNumParallel}
427
- volumes:
428
- - ollama-data:/root/.ollama
429
- entrypoint:
430
- - /bin/sh
431
- - -c
432
- - |
433
- ollama serve &
434
- until ollama list > /dev/null 2>&1; do sleep 1; done
435
- ollama pull ${ollamaModelTag}
436
- wait
437
- healthcheck:
438
- test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
439
- interval: 10s
440
- timeout: 5s
441
- retries: 10
442
- start_period: 30s
443
-
444
- volumes:
445
- ollama-data:`;
446
- } else {
447
- compose = `name: ${multiComposeName}
448
- services:
449
- ai-bot:
450
- build: .
451
- container_name: ${multiAppContainerName}
452
- restart: always
453
- env_file:
454
- - ../../.env
455
- ${appEnvironmentBlock}${extraHosts} volumes:
456
- - ${volumeMount}
457
- ports:
458
- - "${gatewayPort}:${gatewayPort}"`;
459
- }
460
- } else if (is9Router) {
461
- compose = `name: ${singleComposeName}
462
- services:
463
- ai-bot:
464
- build: .
465
- container_name: ${singleAppContainerName}
466
- restart: always
467
- env_file:
468
- - ../../.env
469
- depends_on:
470
- - 9router
471
- ${appEnvironmentBlock}${extraHostsBlock}\n volumes:
472
- - ${volumeMount}
473
- - openclaw-plugins:/home/node/project/.openclaw/npm
474
- - openclaw-extensions:/home/node/project/.openclaw/extensions
475
- ports:
476
- - "${gatewayPort}:${gatewayPort}"
477
-
478
- 9router:
479
- image: node:22-slim
480
- container_name: ${singleRouterContainerName}
481
- restart: always
482
- entrypoint:
483
- - /bin/sh
484
- - -c
485
- - |
486
- ${indentBlock(docker9RouterEntrypointScript, 8)}
487
- environment:
488
- - PORT=${routerPort}
489
- - HOSTNAME=0.0.0.0
490
- - CI=true
491
- volumes:
492
- - 9router-data:/root/.9router
493
- - ./sync.js:/tmp/sync.js:ro
494
- - ./patch-9router.js:/tmp/patch-9router.js:ro
495
- ports:
496
- - "${routerPort}:${routerPort}"
497
-
498
- volumes:
499
- 9router-data:
500
- openclaw-plugins:
501
- openclaw-extensions:`;
502
- } else if (isLocal) {
503
- const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
504
- compose = `name: ${singleComposeName}
505
- services:
506
- ai-bot:
507
- build: .
508
- container_name: ${singleAppContainerName}
509
- restart: always
510
- env_file: ../../.env
511
- ${appEnvironmentBlock} depends_on:
512
- ollama:
513
- condition: service_healthy
514
- ${extraHostsBlock}\n ports:
515
- - "${gatewayPort}:${gatewayPort}"
516
- volumes:
517
- - ${volumeMount}
518
-
519
- ollama:
520
- image: ollama/ollama:latest
521
- container_name: ${singleOllamaContainerName}
522
- restart: always
523
- environment:
524
- - OLLAMA_KEEP_ALIVE=24h
525
- - OLLAMA_NUM_PARALLEL=${singleOllamaNumParallel}
526
- volumes:
527
- - ollama-data:/root/.ollama
528
- entrypoint:
529
- - /bin/sh
530
- - -c
531
- - |
532
- ollama serve &
533
- until ollama list > /dev/null 2>&1; do sleep 1; done
534
- ollama pull ${ollamaModelTag}
535
- wait
536
- healthcheck:
537
- test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
538
- interval: 10s
539
- timeout: 5s
540
- retries: 10
541
- start_period: 30s
542
-
543
- volumes:
544
- ollama-data:`;
545
- } else {
546
- compose = `name: ${singleComposeName}
547
- services:
548
- ai-bot:
549
- build: .
550
- container_name: ${singleAppContainerName}
551
- restart: always
552
- env_file:
553
- - ../../.env
554
- ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
555
- - ${volumeMount}
556
- ports:
557
- - "${gatewayPort}:${gatewayPort}"`;
558
- }
559
-
560
- return {
561
- dockerfile,
562
- compose,
563
- entrypointScript: runtimeScript,
564
- syncScript,
565
- patchScript,
566
- docker9RouterEntrypointScript,
567
- gatewayPatchCmd: buildGatewayPatchCmd(),
568
- };
569
- }
570
-
571
- root.__openclawDockerGen = {
572
- encodeBase64Utf8,
573
- indentBlock,
574
- build9RouterSmartRouteSyncScript,
575
- build9RouterPatchScript,
576
- build9RouterComposeEntrypointScript,
577
- buildGatewayPatchCmd,
578
- buildDockerArtifacts,
579
- };
580
-
581
- })(typeof globalThis !== 'undefined' ? globalThis : {});
582
- if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
583
- Object.assign(exports, globalThis.__openclawDockerGen);
584
- }
585
-
586
-
1
+ // @ts-nocheck
2
+ (function (root) {
3
+ const common = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon) || {};
4
+ const SUPPORTED_CODEX_MODELS = common.SUPPORTED_CODEX_MODELS || ['cx/gpt-5.4', 'cx/gpt-5.3-codex', 'cx/gpt-5.2', 'cx/gpt-5.4-mini'];
5
+ const SMART_ROUTE_PROVIDER_MODELS = common.SMART_ROUTE_PROVIDER_MODELS || { codex: SUPPORTED_CODEX_MODELS };
6
+ const SMART_ROUTE_PROVIDER_ORDER = common.SMART_ROUTE_PROVIDER_ORDER || ['codex'];
7
+
8
+ function encodeBase64Utf8(value) {
9
+ if (typeof Buffer !== 'undefined') {
10
+ return Buffer.from(String(value), 'utf8').toString('base64');
11
+ }
12
+ return btoa(String.fromCharCode(...new TextEncoder().encode(String(value))));
13
+ }
14
+
15
+ function indentBlock(text, spaces) {
16
+ const prefix = ' '.repeat(spaces);
17
+ return String(text).split('\n').map((line) => `${prefix}${line}`).join('\n');
18
+ }
19
+
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');
120
+ }
121
+
122
+ function build9RouterPatchScript() {
123
+ return `const fs=require('fs');const path=require('path');const cp=require('child_process');
124
+ const MODELS=${JSON.stringify(SUPPORTED_CODEX_MODELS.map((model) => model.replace('cx/', '')))};
125
+ const MODEL_NAMES={"gpt-5.4":"GPT 5.4","gpt-5.4-mini":"GPT 5.4 Mini","gpt-5.3-codex":"GPT 5.3 Codex","gpt-5.2":"GPT 5.2"};
126
+ const SELF_TEST_BLOCK=[
127
+ 'codex: {',
128
+ ' url: "https://chatgpt.com/backend-api/codex/responses",',
129
+ ' method: "POST",',
130
+ ' authHeader: "Authorization",',
131
+ ' authPrefix: "Bearer ",',
132
+ ' extraHeaders: { "Content-Type": "application/json", "originator": "codex-cli", "User-Agent": "codex-cli/1.0.18 (macOS; arm64)" },',
133
+ ' body: JSON.stringify({',
134
+ ' model: "gpt-5.2",',
135
+ ' instructions: "You are a coding assistant.",',
136
+ ' input: [{ role: "user", content: [{ type: "input_text", text: "Reply with exactly: ok" }] }],',
137
+ ' stream: true,',
138
+ ' store: false,',
139
+ ' }),',
140
+ ' acceptStatuses: [200, 400],',
141
+ ' refreshable: true,',
142
+ ' },'
143
+ ].join('\\n');
144
+ const roots=new Set();
145
+ function add(p){if(p)roots.add(p);}
146
+ try{const npmRoot=cp.execSync('npm root -g',{stdio:['ignore','pipe','ignore'],encoding:'utf8'}).trim();if(npmRoot)add(path.join(npmRoot,'9router'));}catch{}
147
+ add(path.join(process.env.APPDATA||'','npm','node_modules','9router'));
148
+ add('/usr/local/lib/node_modules/9router');
149
+ add('/usr/lib/node_modules/9router');
150
+ add(path.join(process.cwd(),'node_modules','9router'));
151
+ function patchFile(filePath, transform){if(!fs.existsSync(filePath))return false;const before=fs.readFileSync(filePath,'utf8');const after=transform(before);if(!after||after===before)return false;fs.writeFileSync(filePath,after);return true;}
152
+ function patchText(text,replacers){let next=text;for(const replacer of replacers){next=replacer(next);}return next===text?null:next;}
153
+ function patchProviderModels(root){return patchFile(path.join(root,'open-sse','config','providerModels.js'),(text)=>text.replace(/cx:\\s*\\[[\\s\\S]*?\\],/,()=>{const lines=MODELS.map((id)=>' { id: "'+id+'", name: "'+(MODEL_NAMES[id]||id)+'" },');return 'cx: [ // OpenAI Codex\\n'+lines.join('\\n')+'\\n ],';}));}
154
+ function patchCodexLikeFile(filePath){return patchFile(filePath,(text)=>{if(text.includes('max_output_tokens'))return text;return patchText(text,[
155
+ (value)=>value.replace(/delete (\\w+)\\.max_tokens,delete \\1\\.user/g,'delete $1.max_tokens,delete $1.max_output_tokens,delete $1.user'),
156
+ (value)=>value.replace(/delete (\\w+)\\.max_tokens;(\\s*)delete \\1\\.user/g,'delete $1.max_tokens;$2delete $1.max_output_tokens;$2delete $1.user'),
157
+ (value)=>value.replace(' delete body.max_tokens;\\n',' delete body.max_tokens;\\n delete body.max_output_tokens;\\n')
158
+ ]);});}
159
+ function patchCodexExecutor(root){let touched=0;touched+=patchCodexLikeFile(path.join(root,'open-sse','executors','codex.js'))?1:0;const chunksDir=path.join(root,'app','.next','server','chunks');if(fs.existsSync(chunksDir)){for(const entry of fs.readdirSync(chunksDir)){if(!entry.endsWith('.js'))continue;touched+=patchCodexLikeFile(path.join(chunksDir,entry))?1:0;}}return touched;}
160
+ function patchResponsesNullGuard(root){let touched=0;const chunksDir=path.join(root,'app','.next','server','chunks');if(!fs.existsSync(chunksDir))return touched;for(const entry of fs.readdirSync(chunksDir)){if(!entry.endsWith('.js'))continue;touched+=patchFile(path.join(chunksDir,entry),(text)=>patchText(text,[
161
+ (value)=>value.replace('let b=a.content.find(a=>"output_text"===a.type);','let b=a.content.find(a=>a&&"output_text"===a.type);'),
162
+ (value)=>value.replace('let c=a.content.find(a=>"string"==typeof a.text);','let c=a.content.find(a=>a&&"string"==typeof a.text);'),
163
+ (value)=>value.replace('let b=a.filter(a=>a?.type==="message");','let b=a.filter(a=>a&&a?.type==="message");'),
164
+ (value)=>value.replace('for(let a of j){let b=a.type||(a.role?"message":null);','for(let a of j){let b=a&&(a.type||(a.role?"message":null));'),
165
+ (value)=>value.replace('for(let a of b.messages||[]){if("system"===a.role){','for(let a of b.messages||[])if(a){if("system"===a.role){'),
166
+ (value)=>value.replace('let b=Array.isArray(a.content)?a.content.map(a=>"input_text"===a.type||"output_text"===a.type?{type:"text",text:a.text}:"input_image"===a.type?{type:"image_url",image_url:{url:a.image_url||a.file_id||"",detail:a.detail||"auto"}}:a):a.content;','let b=Array.isArray(a.content)?a.content.map(a=>a&&("input_text"===a.type||"output_text"===a.type)?{type:"text",text:a.text}:a&&"input_image"===a.type?{type:"image_url",image_url:{url:a.image_url||a.file_id||"",detail:a.detail||"auto"}}:a).filter(Boolean):a.content;'),
167
+ (value)=>value.replace('c="string"==typeof a.content?[{type:b,text:a.content}]:Array.isArray(a.content)?a.content.map(a=>{if("text"===a.type)return{type:b,text:a.text};if("image_url"===a.type)return{type:"input_image",image_url:"string"==typeof a.image_url?a.image_url:a.image_url?.url,detail:a.image_url?.detail||"auto"};if("input_image"===a.type)return a;let c=a.text||a.content||JSON.stringify(a);return{type:b,text:"string"==typeof c?c:JSON.stringify(c)}}):[];','c="string"==typeof a.content?[{type:b,text:a.content}]:Array.isArray(a.content)?a.content.map(a=>{if(!a)return null;if("text"===a.type)return{type:b,text:a.text};if("image_url"===a.type)return{type:"input_image",image_url:"string"==typeof a.image_url?a.image_url:a.image_url?.url,detail:a.image_url?.detail||"auto"};if("input_image"===a.type)return a;let c=a.text||a.content||JSON.stringify(a);return{type:b,text:"string"==typeof c?c:JSON.stringify(c)}}).filter(Boolean):[];'),
168
+ (value)=>value.replace('b.tools&&Array.isArray(b.tools)&&(e.tools=b.tools.map(a=>{if(a.function)return a;let b=a.name;return b&&"string"==typeof b&&""!==b.trim()?{type:"function",function:{name:b,description:String(a.description||""),parameters:i(a.parameters),strict:a.strict}}:null}).filter(Boolean))','b.tools&&Array.isArray(b.tools)&&(e.tools=b.tools.map(a=>{if(!a)return null;if(a.function)return a;let b=a.name;return b&&"string"==typeof b&&""!==b.trim()?{type:"function",function:{name:b,description:String(a.description||""),parameters:i(a.parameters),strict:a.strict}}:null}).filter(Boolean))'),
169
+ (value)=>value.replace('b.tools&&Array.isArray(b.tools)&&(e.tools=b.tools.map(a=>"function"===a.type?{type:"function",name:a.function.name,description:String(a.function.description||""),parameters:i(a.function.parameters),strict:a.function.strict}:a)),','b.tools&&Array.isArray(b.tools)&&(e.tools=b.tools.map(a=>a&&"function"===a.type?{type:"function",name:a.function.name,description:String(a.function.description||""),parameters:i(a.function.parameters),strict:a.function.strict}:a).filter(Boolean)),'),
170
+ (value)=>value.replace('filter(a=>"function_call"===a.type)','filter(a=>a&&"function_call"===a.type)'),
171
+ (value)=>value.replace(/filter\\(a=>"text"===a\\.type\\)/g,'filter(a=>a&&"text"===a.type)'),
172
+ (value)=>value.replace(/find\\(a=>"message_stop"===a\\.type\\)/g,'find(a=>a&&"message_stop"===a.type)'),
173
+ (value)=>value.replace(/find\\(a=>"content_block_delta"===a\\.type\\)/g,'find(a=>a&&"content_block_delta"===a.type)'),
174
+ (value)=>value.replace(/find\\(a=>"message_delta"===a\\.type\\)/g,'find(a=>a&&"message_delta"===a.type)'),
175
+ (value)=>value.replace(/find\\(a=>"message_start"===a\\.type\\)/g,'find(a=>a&&"message_start"===a.type)'),
176
+ (value)=>value.replace(/for\\(let e of a\\.content\\)(?!if\\(e\\))/g,'for(let e of a.content)if(e)')
177
+ ] ))?1:0;}return touched;}
178
+ function patchSelfTest(root){return patchFile(path.join(root,'src','app','api','providers','[id]','test','testUtils.js'),(text)=>{if(text.includes('model: "gpt-5.2"')&&text.includes('store: false')&&text.includes('acceptStatuses: [200, 400]'))return text;return text.replace(/codex:\\s*\\{[\\s\\S]*?refreshable:\\s*true,\\s*\\},/,SELF_TEST_BLOCK);});}
179
+ let touched=0;
180
+ for(const root of roots){if(!root||!fs.existsSync(root))continue;touched+=patchProviderModels(root)?1:0;touched+=patchCodexExecutor(root)?1:0;touched+=patchResponsesNullGuard(root)?1:0;touched+=patchSelfTest(root)?1:0;}
181
+ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}else{console.log('[patch-9router] No compatible 9router source files found to patch.');}`;
182
+ }
183
+
184
+ function build9RouterComposeEntrypointScript(routerPort) {
185
+ const port = routerPort || 20128;
186
+ const nineRouterSpec = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon && globalThis.__openclawCommon.NINE_ROUTER_NPM_SPEC) || '9router@latest';
187
+ return [
188
+ `npm install -g ` + nineRouterSpec,
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',
191
+ 'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
192
+ `exec 9router -n -l -H 0.0.0.0 -p ${port} --skip-update`
193
+ ].join('\n');
194
+ }
195
+
196
+ function buildGatewayPatchCmd() {
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));}\\"`;
198
+ }
199
+
200
+ function buildDockerArtifacts(options) {
201
+ const {
202
+ openClawNpmSpec,
203
+ openClawRuntimePackages,
204
+ is9Router,
205
+ isLocal,
206
+ isMultiBot,
207
+ hasBrowser = false,
208
+ selectedModel,
209
+ agentId,
210
+ allSkills = [],
211
+ dockerfilePlugins = [],
212
+ dockerfileSkillInstallMode = 'none',
213
+ runtimeCommandParts = [],
214
+ volumeMount = '../../.openclaw:/home/node/project/.openclaw\n - ../../:/mnt/project',
215
+ singleComposeName = 'oc-bot',
216
+ multiComposeName = 'oc-multibot',
217
+ singleAppContainerName = 'openclaw-bot',
218
+ multiAppContainerName = 'openclaw-multibot',
219
+ singleRouterContainerName = '9router',
220
+ multiRouterContainerName = '9router-multibot',
221
+ singleOllamaContainerName = 'ollama',
222
+ multiOllamaContainerName = 'ollama-multibot',
223
+ plainSingleExtraHosts = false,
224
+ multiOllamaNumParallel = 1,
225
+ singleOllamaNumParallel = 1,
226
+ gatewayPort = 18789,
227
+ routerPort = 20128,
228
+ osChoice = '',
229
+ } = options;
230
+ // Windows bind-mounts give ClawHub-installed plugins world-writable perms (which openclaw
231
+ // blocks), so on Windows we isolate extensions in a named volume. On macOS/Linux bind-mounts
232
+ // are fine, so keep extensions under the .openclaw bind mount → plugins stay visible/synced
233
+ // on the host (e.g. you can see zalo-mod in .openclaw/extensions).
234
+ const useExtensionsVolume = osChoice === 'win';
235
+ const extVolMount = useExtensionsVolume ? '\n - openclaw-extensions:/home/node/project/.openclaw/extensions' : '';
236
+ const extVolDecl = useExtensionsVolume ? '\n openclaw-extensions:' : '';
237
+ const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
238
+ ? `\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`
239
+ : '';
240
+ const pluginLines = dockerfilePlugins.length > 0
241
+ ? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
242
+ : '';
243
+ const patchLine = `RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"`;
244
+
245
+ // Dynamic runtime configuration: backup config before any first-run install, restore after.
246
+ // Missing plugin install may touch openclaw.json, so preserve critical fields.
247
+ 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);}`;
248
+
249
+ 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);}`;
250
+ 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));}}`;
251
+
252
+ const runtimeParts = runtimeCommandParts.filter(Boolean);
253
+ const runtimePrelude = [
254
+ 'export OPENCLAW_HOME="${OPENCLAW_HOME:-$PWD/.openclaw}"',
255
+ 'export OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"',
256
+ 'mkdir -p "$OPENCLAW_HOME" "$OPENCLAW_STATE_DIR"',
257
+ 'if [ "$OPENCLAW_STATE_DIR" != "$OPENCLAW_HOME" ]; then',
258
+ ' for path in "$OPENCLAW_HOME"/*; do',
259
+ ' [ -e "$path" ] || continue',
260
+ ' name="$(basename "$path")"',
261
+ ' [ "$name" = "plugin-runtime-deps" ] && continue',
262
+ ' [ "$name" = "logs" ] && continue',
263
+ ' [ -e "$OPENCLAW_STATE_DIR/$name" ] || ln -s "$path" "$OPENCLAW_STATE_DIR/$name"',
264
+ ' done',
265
+ 'fi',
266
+ 'ensure_plugin() {',
267
+ ' id="$1"',
268
+ ' spec="$2"',
269
+ ' if [ -d "$OPENCLAW_HOME/extensions/$id" ]; then',
270
+ ' echo "[entrypoint] plugin $id already installed"',
271
+ ' return 0',
272
+ ' fi',
273
+ ' echo "[entrypoint] plugin $id missing; installing $spec"',
274
+ ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
275
+ '}',
276
+ 'ensure_zalouser() {',
277
+ ' NPM_DIR="$OPENCLAW_HOME/npm"',
278
+ ' PKG_DIR="$NPM_DIR/node_modules/@openclaw/zalouser"',
279
+ ' if [ -d "$PKG_DIR" ] || [ -d "$OPENCLAW_HOME/extensions/zalouser" ]; then',
280
+ ' echo "[entrypoint] zalouser plugin already installed (npm or extensions) — skipping"',
281
+ ' else',
282
+ ' echo "[entrypoint] zalouser plugin missing; installing via npm"',
283
+ ' mkdir -p "$NPM_DIR"',
284
+ ' cd "$NPM_DIR"',
285
+ ' npm init -y 2>/dev/null || true',
286
+ ' npm install @openclaw/zalouser@latest 2>/dev/null || echo "[entrypoint] warning: failed to install @openclaw/zalouser"',
287
+ ' cd /home/node/project',
288
+ ' fi',
289
+ '}',
290
+ 'ensure_skill() {',
291
+ ' id="$1"',
292
+ ' if find "$OPENCLAW_HOME" -maxdepth 4 -type d -path "*/skills/$id" -print -quit 2>/dev/null | grep -q .; then',
293
+ ' echo "[entrypoint] skill $id already installed"',
294
+ ' return 0',
295
+ ' fi',
296
+ ' echo "[entrypoint] skill $id missing; installing"',
297
+ ' openclaw skills install "$id" 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
298
+ '}',
299
+ 'echo "[entrypoint] ensuring runtime assets, then starting gateway"',
300
+ ];
301
+ runtimeParts.unshift(...runtimePrelude);
302
+ // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
303
+ runtimeParts.unshift(`node - <<'NODE'\n${backupConfigScript}\nNODE`);
304
+ // Restore config AFTER plugin installs (which may clobber openclaw.json)
305
+ runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
306
+ runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
307
+ // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
308
+ runtimeParts.push([
309
+ '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
310
+ 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
311
+ 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
312
+ ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
313
+ ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
314
+ 'fi',
315
+ '# Automatically run Zalo sticker-mention patch if skill is present',
316
+ 'for MENTIONS_JS in $(find "$OPENCLAW_HOME" -maxdepth 5 -path "*/skills/*/mentions.js" -type f 2>/dev/null); do',
317
+ ' if [ -f "$MENTIONS_JS" ]; then',
318
+ ' echo "[entrypoint] Running patch: $MENTIONS_JS"',
319
+ ' node "$MENTIONS_JS" || echo "[entrypoint] Warning: failed to run patch $MENTIONS_JS"',
320
+ ' fi',
321
+ 'done',
322
+ ].join('\n'));
323
+ // Expose zalouser's ZCA API map (globalThis.__zcaApiByProfile) BEFORE the gateway imports
324
+ // zalouser, so openclaw-zalo-mod can reach the live Zalo API (dashboard Sync Account, group
325
+ // admin lookups, …). zalo-mod's own runtime patch lands AFTER zalouser is already imported,
326
+ // so it never takes effect on the running module — patching here (post sticker-mention restore,
327
+ // pre gateway-run) is the reliable fix.
328
+ {
329
+ const exposeZcaScript = [
330
+ "const fs=require('fs'),cp=require('child_process');",
331
+ "let files=[];",
332
+ "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){}",
333
+ "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());']];",
334
+ "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){} }",
335
+ ].join('\n');
336
+ runtimeParts.push(`# Expose zalouser ZCA API map for openclaw-zalo-mod (before gateway imports zalouser)\nnode - <<'NODE'\n${exposeZcaScript}\nNODE`);
337
+ }
338
+ runtimeParts.push([
339
+ '# Zalo channel auto-restart monitor (background)',
340
+ '(',
341
+ ' sleep 180',
342
+ ' while true; do',
343
+ ' sleep 60',
344
+ ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
345
+ ' if echo "$STATUS" | grep -qi "stopped"; then',
346
+ ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
347
+ ' sleep 5',
348
+ ' kill 1 2>/dev/null || true',
349
+ ' fi',
350
+ ' done',
351
+ ') &',
352
+ ].join('\n'));
353
+ runtimeParts.push('openclaw gateway run');
354
+ const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
355
+ let browserInstall = '';
356
+ if (hasBrowser) {
357
+ browserInstall = '\n# Install browser and system dependencies for Playwright\nRUN npx playwright install-deps chromium && npx playwright install chromium\n';
358
+ }
359
+ const dockerfile = `FROM node:22-slim
360
+
361
+ RUN apt-get update && apt-get install -y git curl python3 && rm -rf /var/lib/apt/lists/*
362
+
363
+ ARG OPENCLAW_VER="${openClawNpmSpec}"
364
+ ARG CACHE_BUST=""
365
+ RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
366
+ ${patchLine}${browserInstall}
367
+
368
+ COPY entrypoint.sh /usr/local/bin/openclaw-entrypoint.sh
369
+ RUN chmod +x /usr/local/bin/openclaw-entrypoint.sh
370
+ WORKDIR /home/node/project
371
+
372
+ EXPOSE ${gatewayPort}
373
+
374
+ CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
375
+
376
+ const syncScript = build9RouterSmartRouteSyncScript();
377
+ const patchScript = build9RouterPatchScript();
378
+ const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
379
+ const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
380
+
381
+ const appEnvironmentBlock = ` environment:\n - HOME=/home/node/project/.openclaw\n - OPENCLAW_HOME=/home/node/project/.openclaw\n - OPENCLAW_STATE_DIR=/home/node/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n - OPENCLAW_SETUP_OS=${osChoice || ''}\n - OPENCLAW_BROWSER_HOST_OS=${osChoice || ''}\n - OPENCLAW_GATEWAY_PORT=${gatewayPort}\n - OPENCLAW_PORT=${gatewayPort}\n tmpfs:\n - /home/node/project/.openclaw/plugin-runtime-deps\n`;
382
+
383
+ let compose;
384
+ if (isMultiBot) {
385
+ const dependsOn = is9Router
386
+ ? ' depends_on:\n - 9router\n'
387
+ : isLocal
388
+ ? ' depends_on:\n ollama:\n condition: service_healthy\n'
389
+ : '';
390
+ const extraHosts = `${extraHostsBlock}\n`;
391
+ if (is9Router) {
392
+ compose = `name: ${multiComposeName}
393
+ services:
394
+ ai-bot:
395
+ build: .
396
+ container_name: ${multiAppContainerName}
397
+ restart: always
398
+ env_file:
399
+ - ../../.env
400
+ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
401
+ - ${volumeMount}
402
+ ports:
403
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"
404
+
405
+ 9router:
406
+ image: node:22-slim
407
+ container_name: ${multiRouterContainerName}
408
+ restart: always
409
+ entrypoint:
410
+ - /bin/sh
411
+ - -c
412
+ - |
413
+ ${indentBlock(docker9RouterEntrypointScript, 8)}
414
+ environment:
415
+ - PORT=${routerPort}
416
+ - HOSTNAME=0.0.0.0
417
+ - CI=true
418
+ volumes:
419
+ - 9router-data:/root/.9router
420
+ - ./sync.js:/tmp/sync.js:ro
421
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
422
+ ports:
423
+ - "127.0.0.1:${routerPort}:${routerPort}"
424
+
425
+ volumes:
426
+ 9router-data:`;
427
+ } else if (isLocal) {
428
+ const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
429
+ compose = `name: ${multiComposeName}
430
+ services:
431
+ ai-bot:
432
+ build: .
433
+ container_name: ${multiAppContainerName}
434
+ restart: always
435
+ env_file:
436
+ - ../../.env
437
+ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
438
+ - ${volumeMount}
439
+ ports:
440
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"
441
+
442
+ ollama:
443
+ image: ollama/ollama:latest
444
+ container_name: ${multiOllamaContainerName}
445
+ restart: always
446
+ environment:
447
+ - OLLAMA_KEEP_ALIVE=24h
448
+ - OLLAMA_NUM_PARALLEL=${multiOllamaNumParallel}
449
+ volumes:
450
+ - ollama-data:/root/.ollama
451
+ entrypoint:
452
+ - /bin/sh
453
+ - -c
454
+ - |
455
+ ollama serve &
456
+ until ollama list > /dev/null 2>&1; do sleep 1; done
457
+ ollama pull ${ollamaModelTag}
458
+ wait
459
+ healthcheck:
460
+ test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
461
+ interval: 10s
462
+ timeout: 5s
463
+ retries: 10
464
+ start_period: 30s
465
+
466
+ volumes:
467
+ ollama-data:`;
468
+ } else {
469
+ compose = `name: ${multiComposeName}
470
+ services:
471
+ ai-bot:
472
+ build: .
473
+ container_name: ${multiAppContainerName}
474
+ restart: always
475
+ env_file:
476
+ - ../../.env
477
+ ${appEnvironmentBlock}${extraHosts} volumes:
478
+ - ${volumeMount}
479
+ ports:
480
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"`;
481
+ }
482
+ } else if (is9Router) {
483
+ compose = `name: ${singleComposeName}
484
+ services:
485
+ ai-bot:
486
+ build: .
487
+ container_name: ${singleAppContainerName}
488
+ restart: always
489
+ env_file:
490
+ - ../../.env
491
+ depends_on:
492
+ - 9router
493
+ ${appEnvironmentBlock}${extraHostsBlock}\n volumes:
494
+ - ${volumeMount}
495
+ - openclaw-plugins:/home/node/project/.openclaw/npm${extVolMount}
496
+ ports:
497
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"
498
+
499
+ 9router:
500
+ image: node:22-slim
501
+ container_name: ${singleRouterContainerName}
502
+ restart: always
503
+ entrypoint:
504
+ - /bin/sh
505
+ - -c
506
+ - |
507
+ ${indentBlock(docker9RouterEntrypointScript, 8)}
508
+ environment:
509
+ - PORT=${routerPort}
510
+ - HOSTNAME=0.0.0.0
511
+ - CI=true
512
+ volumes:
513
+ - 9router-data:/root/.9router
514
+ - ./sync.js:/tmp/sync.js:ro
515
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
516
+ ports:
517
+ - "127.0.0.1:${routerPort}:${routerPort}"
518
+
519
+ volumes:
520
+ 9router-data:
521
+ openclaw-plugins:${extVolDecl}`;
522
+ } else if (isLocal) {
523
+ const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
524
+ compose = `name: ${singleComposeName}
525
+ services:
526
+ ai-bot:
527
+ build: .
528
+ container_name: ${singleAppContainerName}
529
+ restart: always
530
+ env_file: ../../.env
531
+ ${appEnvironmentBlock} depends_on:
532
+ ollama:
533
+ condition: service_healthy
534
+ ${extraHostsBlock}\n ports:
535
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"
536
+ volumes:
537
+ - ${volumeMount}
538
+
539
+ ollama:
540
+ image: ollama/ollama:latest
541
+ container_name: ${singleOllamaContainerName}
542
+ restart: always
543
+ environment:
544
+ - OLLAMA_KEEP_ALIVE=24h
545
+ - OLLAMA_NUM_PARALLEL=${singleOllamaNumParallel}
546
+ volumes:
547
+ - ollama-data:/root/.ollama
548
+ entrypoint:
549
+ - /bin/sh
550
+ - -c
551
+ - |
552
+ ollama serve &
553
+ until ollama list > /dev/null 2>&1; do sleep 1; done
554
+ ollama pull ${ollamaModelTag}
555
+ wait
556
+ healthcheck:
557
+ test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
558
+ interval: 10s
559
+ timeout: 5s
560
+ retries: 10
561
+ start_period: 30s
562
+
563
+ volumes:
564
+ ollama-data:`;
565
+ } else {
566
+ compose = `name: ${singleComposeName}
567
+ services:
568
+ ai-bot:
569
+ build: .
570
+ container_name: ${singleAppContainerName}
571
+ restart: always
572
+ env_file:
573
+ - ../../.env
574
+ ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
575
+ - ${volumeMount}
576
+ ports:
577
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"`;
578
+ }
579
+
580
+ return {
581
+ dockerfile,
582
+ compose,
583
+ entrypointScript: runtimeScript,
584
+ syncScript,
585
+ patchScript,
586
+ docker9RouterEntrypointScript,
587
+ gatewayPatchCmd: buildGatewayPatchCmd(),
588
+ };
589
+ }
590
+
591
+ root.__openclawDockerGen = {
592
+ encodeBase64Utf8,
593
+ indentBlock,
594
+ build9RouterSmartRouteSyncScript,
595
+ build9RouterPatchScript,
596
+ build9RouterComposeEntrypointScript,
597
+ buildGatewayPatchCmd,
598
+ buildDockerArtifacts,
599
+ };
600
+
601
+ })(typeof globalThis !== 'undefined' ? globalThis : {});
602
+ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
603
+ Object.assign(exports, globalThis.__openclawDockerGen);
604
+ }
605
+
606
+