create-openclaw-bot 5.8.0 → 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.
@@ -1,576 +1,571 @@
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 + ` better-sqlite3`,
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,
208
- selectedModel,
209
- agentId,
210
- allSkills = [],
211
- dockerfilePlugins = [],
212
- dockerfileSkillInstallMode = 'none',
213
- runtimeCommandParts = [],
214
- volumeMount = '../../.openclaw:/root/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
- emitBrowserInstall = true,
227
- gatewayPort = 18789,
228
- routerPort = 20128,
229
- } = options;
230
-
231
- const browserAptExtra = hasBrowser ? ' xvfb socat' : '';
232
- const browserInstallLines = hasBrowser && emitBrowserInstall
233
- ? [
234
- '',
235
- '# Browser Automation: Playwright engine (needed for native CDP)',
236
- 'RUN npm install -g agent-browser playwright \\',
237
- ' && npx playwright install chromium --with-deps \\',
238
- ' && ln -f -s /root/.cache/ms-playwright/chromium-*/chrome-linux*/chrome /usr/bin/google-chrome',
239
- '',
240
- ''
241
- ].join('\n')
242
- : '';
243
- const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
244
- ? `\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`
245
- : '';
246
- const pluginLines = dockerfilePlugins.length > 0
247
- ? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
248
- : '';
249
- 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);}"`;
250
-
251
- // Dynamic runtime configuration: backup config before any first-run install, restore after.
252
- // Missing plugin install may touch openclaw.json, so preserve critical fields.
253
- 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);}`;
254
-
255
- 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);}`;
256
- 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));}}`;
257
-
258
- const runtimeParts = runtimeCommandParts.filter(Boolean);
259
- const runtimePrelude = [
260
- 'export OPENCLAW_HOME="${OPENCLAW_HOME:-$PWD/.openclaw}"',
261
- 'export OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"',
262
- 'mkdir -p "$OPENCLAW_HOME" "$OPENCLAW_STATE_DIR"',
263
- 'if [ "$OPENCLAW_STATE_DIR" != "$OPENCLAW_HOME" ]; then',
264
- ' for path in "$OPENCLAW_HOME"/*; do',
265
- ' [ -e "$path" ] || continue',
266
- ' name="$(basename "$path")"',
267
- ' [ "$name" = "plugin-runtime-deps" ] && continue',
268
- ' [ "$name" = "logs" ] && continue',
269
- ' [ -e "$OPENCLAW_STATE_DIR/$name" ] || ln -s "$path" "$OPENCLAW_STATE_DIR/$name"',
270
- ' done',
271
- 'fi',
272
- 'ensure_plugin() {',
273
- ' id="$1"',
274
- ' spec="$2"',
275
- ' if [ -d "$OPENCLAW_HOME/extensions/$id" ]; then',
276
- ' echo "[entrypoint] plugin $id already installed"',
277
- ' return 0',
278
- ' fi',
279
- ' echo "[entrypoint] plugin $id missing; installing $spec"',
280
- ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
281
- '}',
282
- 'ensure_skill() {',
283
- ' id="$1"',
284
- ' if find "$OPENCLAW_HOME" -maxdepth 4 -type d -path "*/skills/$id" -print -quit 2>/dev/null | grep -q .; then',
285
- ' echo "[entrypoint] skill $id already installed"',
286
- ' return 0',
287
- ' fi',
288
- ' echo "[entrypoint] skill $id missing; installing"',
289
- ' openclaw skills install "$id" 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
290
- '}',
291
- 'echo "[entrypoint] ensuring runtime assets, then starting gateway"',
292
- ];
293
- runtimeParts.unshift(...runtimePrelude);
294
- // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
295
- runtimeParts.unshift(`node - <<'NODE'\n${backupConfigScript}\nNODE`);
296
- // Restore config AFTER plugin installs (which may clobber openclaw.json)
297
- runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
298
- runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
299
- // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
300
- runtimeParts.push([
301
- '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
302
- 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
303
- 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
304
- ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
305
- ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
306
- 'fi',
307
- ].join('\\n'));
308
- runtimeParts.push([
309
- '# Zalo channel auto-restart monitor (background)',
310
- '(',
311
- ' sleep 180',
312
- ' while true; do',
313
- ' sleep 60',
314
- ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
315
- ' if echo "$STATUS" | grep -qi "stopped"; then',
316
- ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
317
- ' sleep 5',
318
- ' kill 1 2>/dev/null || true',
319
- ' fi',
320
- ' done',
321
- ') &',
322
- ].join('\\n'));
323
- if (hasBrowser) {
324
- runtimeParts.push('socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 &');
325
- runtimeParts.push('Xvfb :99 -screen 0 1280x720x24 > /dev/null 2>&1 & DISPLAY=:99 openclaw gateway run');
326
- } else {
327
- runtimeParts.push('openclaw gateway run');
328
- }
329
- const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
330
- const dockerfile = `FROM node:22-slim
331
-
332
- RUN apt-get update && apt-get install -y git curl python3${browserAptExtra} && rm -rf /var/lib/apt/lists/*
333
- ${browserInstallLines}
334
- ARG OPENCLAW_VER="${openClawNpmSpec}"
335
- ARG CACHE_BUST=""
336
- RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
337
- ${patchLine}
338
- COPY entrypoint.sh /usr/local/bin/openclaw-entrypoint.sh
339
- RUN chmod +x /usr/local/bin/openclaw-entrypoint.sh
340
- WORKDIR /root/project
341
-
342
- EXPOSE ${gatewayPort}
343
-
344
- CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
345
-
346
- const syncScript = build9RouterSmartRouteSyncScript();
347
- const patchScript = build9RouterPatchScript();
348
- const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
349
- const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
350
-
351
- 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`;
352
-
353
- let compose;
354
- if (isMultiBot) {
355
- const dependsOn = is9Router
356
- ? ' depends_on:\n - 9router\n'
357
- : isLocal
358
- ? ' depends_on:\n ollama:\n condition: service_healthy\n'
359
- : '';
360
- const extraHosts = hasBrowser ? `${extraHostsBlock}\n` : '';
361
- if (is9Router) {
362
- compose = `name: ${multiComposeName}
363
- services:
364
- ai-bot:
365
- build: .
366
- container_name: ${multiAppContainerName}
367
- restart: always
368
- env_file:
369
- - ../../.env
370
- ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
371
- - ${volumeMount}
372
- ports:
373
- - "${gatewayPort}:${gatewayPort}"
374
-
375
- 9router:
376
- image: node:22-slim
377
- container_name: ${multiRouterContainerName}
378
- restart: always
379
- entrypoint:
380
- - /bin/sh
381
- - -c
382
- - |
383
- ${indentBlock(docker9RouterEntrypointScript, 8)}
384
- environment:
385
- - PORT=${routerPort}
386
- - HOSTNAME=0.0.0.0
387
- - CI=true
388
- volumes:
389
- - 9router-data:/root/.9router
390
- - ./sync.js:/tmp/sync.js:ro
391
- - ./patch-9router.js:/tmp/patch-9router.js:ro
392
- ports:
393
- - "${routerPort}:${routerPort}"
394
-
395
- volumes:
396
- 9router-data:`;
397
- } else if (isLocal) {
398
- const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
399
- compose = `name: ${multiComposeName}
400
- services:
401
- ai-bot:
402
- build: .
403
- container_name: ${multiAppContainerName}
404
- restart: always
405
- env_file:
406
- - ../../.env
407
- ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
408
- - ${volumeMount}
409
- ports:
410
- - "${gatewayPort}:${gatewayPort}"
411
-
412
- ollama:
413
- image: ollama/ollama:latest
414
- container_name: ${multiOllamaContainerName}
415
- restart: always
416
- environment:
417
- - OLLAMA_KEEP_ALIVE=24h
418
- - OLLAMA_NUM_PARALLEL=${multiOllamaNumParallel}
419
- volumes:
420
- - ollama-data:/root/.ollama
421
- entrypoint:
422
- - /bin/sh
423
- - -c
424
- - |
425
- ollama serve &
426
- until ollama list > /dev/null 2>&1; do sleep 1; done
427
- ollama pull ${ollamaModelTag}
428
- wait
429
- healthcheck:
430
- test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
431
- interval: 10s
432
- timeout: 5s
433
- retries: 10
434
- start_period: 30s
435
-
436
- volumes:
437
- ollama-data:`;
438
- } else {
439
- compose = `name: ${multiComposeName}
440
- services:
441
- ai-bot:
442
- build: .
443
- container_name: ${multiAppContainerName}
444
- restart: always
445
- env_file:
446
- - ../../.env
447
- ${appEnvironmentBlock}${extraHosts} volumes:
448
- - ${volumeMount}
449
- ports:
450
- - "${gatewayPort}:${gatewayPort}"`;
451
- }
452
- } else if (is9Router) {
453
- compose = `name: ${singleComposeName}
454
- services:
455
- ai-bot:
456
- build: .
457
- container_name: ${singleAppContainerName}
458
- restart: always
459
- env_file:
460
- - ../../.env
461
- depends_on:
462
- - 9router
463
- ${appEnvironmentBlock}${hasBrowser ? `${extraHostsBlock}\n` : ''} volumes:
464
- - ${volumeMount}
465
- - openclaw-plugins:/root/project/.openclaw/npm
466
- ports:
467
- - "${gatewayPort}:${gatewayPort}"
468
-
469
- 9router:
470
- image: node:22-slim
471
- container_name: ${singleRouterContainerName}
472
- restart: always
473
- entrypoint:
474
- - /bin/sh
475
- - -c
476
- - |
477
- ${indentBlock(docker9RouterEntrypointScript, 8)}
478
- environment:
479
- - PORT=${routerPort}
480
- - HOSTNAME=0.0.0.0
481
- - CI=true
482
- volumes:
483
- - 9router-data:/root/.9router
484
- - ./sync.js:/tmp/sync.js:ro
485
- - ./patch-9router.js:/tmp/patch-9router.js:ro
486
- ports:
487
- - "${routerPort}:${routerPort}"
488
-
489
- volumes:
490
- 9router-data:
491
- openclaw-plugins:`;
492
- } else if (isLocal) {
493
- const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
494
- compose = `name: ${singleComposeName}
495
- services:
496
- ai-bot:
497
- build: .
498
- container_name: ${singleAppContainerName}
499
- restart: always
500
- env_file: ../../.env
501
- ${appEnvironmentBlock} depends_on:
502
- ollama:
503
- condition: service_healthy
504
- ${hasBrowser ? `${extraHostsBlock}\n` : ''} ports:
505
- - "${gatewayPort}:${gatewayPort}"
506
- volumes:
507
- - ${volumeMount}
508
-
509
- ollama:
510
- image: ollama/ollama:latest
511
- container_name: ${singleOllamaContainerName}
512
- restart: always
513
- environment:
514
- - OLLAMA_KEEP_ALIVE=24h
515
- - OLLAMA_NUM_PARALLEL=${singleOllamaNumParallel}
516
- volumes:
517
- - ollama-data:/root/.ollama
518
- entrypoint:
519
- - /bin/sh
520
- - -c
521
- - |
522
- ollama serve &
523
- until ollama list > /dev/null 2>&1; do sleep 1; done
524
- ollama pull ${ollamaModelTag}
525
- wait
526
- healthcheck:
527
- test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
528
- interval: 10s
529
- timeout: 5s
530
- retries: 10
531
- start_period: 30s
532
-
533
- volumes:
534
- ollama-data:`;
535
- } else {
536
- compose = `name: ${singleComposeName}
537
- services:
538
- ai-bot:
539
- build: .
540
- container_name: ${singleAppContainerName}
541
- restart: always
542
- env_file:
543
- - ../../.env
544
- ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
545
- - ${volumeMount}
546
- ports:
547
- - "${gatewayPort}:${gatewayPort}"`;
548
- }
549
-
550
- return {
551
- dockerfile,
552
- compose,
553
- entrypointScript: runtimeScript,
554
- syncScript,
555
- patchScript,
556
- docker9RouterEntrypointScript,
557
- gatewayPatchCmd: buildGatewayPatchCmd(),
558
- };
559
- }
560
-
561
- root.__openclawDockerGen = {
562
- encodeBase64Utf8,
563
- indentBlock,
564
- build9RouterSmartRouteSyncScript,
565
- build9RouterPatchScript,
566
- build9RouterComposeEntrypointScript,
567
- buildGatewayPatchCmd,
568
- buildDockerArtifacts,
569
- };
570
-
571
- })(typeof globalThis !== 'undefined' ? globalThis : {});
572
- if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
573
- Object.assign(exports, globalThis.__openclawDockerGen);
574
- }
575
-
576
-
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 + ` better-sqlite3`,
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
+ selectedModel,
208
+ agentId,
209
+ allSkills = [],
210
+ dockerfilePlugins = [],
211
+ dockerfileSkillInstallMode = 'none',
212
+ runtimeCommandParts = [],
213
+ volumeMount = '../../.openclaw:/root/project/.openclaw\n - ../../:/mnt/project',
214
+ singleComposeName = 'oc-bot',
215
+ multiComposeName = 'oc-multibot',
216
+ singleAppContainerName = 'openclaw-bot',
217
+ multiAppContainerName = 'openclaw-multibot',
218
+ singleRouterContainerName = '9router',
219
+ multiRouterContainerName = '9router-multibot',
220
+ singleOllamaContainerName = 'ollama',
221
+ multiOllamaContainerName = 'ollama-multibot',
222
+ plainSingleExtraHosts = false,
223
+ multiOllamaNumParallel = 1,
224
+ singleOllamaNumParallel = 1,
225
+ gatewayPort = 18789,
226
+ routerPort = 20128,
227
+ } = options;
228
+ const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
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`
230
+ : '';
231
+ const pluginLines = dockerfilePlugins.length > 0
232
+ ? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
233
+ : '';
234
+ 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);}"`;
235
+
236
+ // Dynamic runtime configuration: backup config before any first-run install, restore after.
237
+ // Missing plugin install may touch openclaw.json, so preserve critical fields.
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);}`;
239
+
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));}}`;
242
+
243
+ const runtimeParts = runtimeCommandParts.filter(Boolean);
244
+ const runtimePrelude = [
245
+ 'export OPENCLAW_HOME="${OPENCLAW_HOME:-$PWD/.openclaw}"',
246
+ 'export OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"',
247
+ 'mkdir -p "$OPENCLAW_HOME" "$OPENCLAW_STATE_DIR"',
248
+ 'if [ "$OPENCLAW_STATE_DIR" != "$OPENCLAW_HOME" ]; then',
249
+ ' for path in "$OPENCLAW_HOME"/*; do',
250
+ ' [ -e "$path" ] || continue',
251
+ ' name="$(basename "$path")"',
252
+ ' [ "$name" = "plugin-runtime-deps" ] && continue',
253
+ ' [ "$name" = "logs" ] && continue',
254
+ ' [ -e "$OPENCLAW_STATE_DIR/$name" ] || ln -s "$path" "$OPENCLAW_STATE_DIR/$name"',
255
+ ' done',
256
+ 'fi',
257
+ 'ensure_plugin() {',
258
+ ' id="$1"',
259
+ ' spec="$2"',
260
+ ' if [ -d "$OPENCLAW_HOME/extensions/$id" ]; then',
261
+ ' echo "[entrypoint] plugin $id already installed"',
262
+ ' return 0',
263
+ ' fi',
264
+ ' echo "[entrypoint] plugin $id missing; installing $spec"',
265
+ ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
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
+ '}',
281
+ 'ensure_skill() {',
282
+ ' id="$1"',
283
+ ' if find "$OPENCLAW_HOME" -maxdepth 4 -type d -path "*/skills/$id" -print -quit 2>/dev/null | grep -q .; then',
284
+ ' echo "[entrypoint] skill $id already installed"',
285
+ ' return 0',
286
+ ' fi',
287
+ ' echo "[entrypoint] skill $id missing; installing"',
288
+ ' openclaw skills install "$id" 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
289
+ '}',
290
+ 'echo "[entrypoint] ensuring runtime assets, then starting gateway"',
291
+ ];
292
+ runtimeParts.unshift(...runtimePrelude);
293
+ // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
294
+ runtimeParts.unshift(`node - <<'NODE'\n${backupConfigScript}\nNODE`);
295
+ // Restore config AFTER plugin installs (which may clobber openclaw.json)
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');
323
+ const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
324
+ const dockerfile = `FROM node:22-slim
325
+
326
+ RUN apt-get update && apt-get install -y git curl python3 && rm -rf /var/lib/apt/lists/*
327
+
328
+ ARG OPENCLAW_VER="${openClawNpmSpec}"
329
+ ARG CACHE_BUST=""
330
+ RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
331
+ ${patchLine}
332
+
333
+ COPY entrypoint.sh /usr/local/bin/openclaw-entrypoint.sh
334
+ RUN chmod +x /usr/local/bin/openclaw-entrypoint.sh
335
+ WORKDIR /root/project
336
+
337
+ EXPOSE ${gatewayPort}
338
+
339
+ CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
340
+
341
+ const syncScript = build9RouterSmartRouteSyncScript();
342
+ const patchScript = build9RouterPatchScript();
343
+ const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
344
+ const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
345
+
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`;
347
+
348
+ let compose;
349
+ if (isMultiBot) {
350
+ const dependsOn = is9Router
351
+ ? ' depends_on:\n - 9router\n'
352
+ : isLocal
353
+ ? ' depends_on:\n ollama:\n condition: service_healthy\n'
354
+ : '';
355
+ const extraHosts = `${extraHostsBlock}\n`;
356
+ if (is9Router) {
357
+ compose = `name: ${multiComposeName}
358
+ services:
359
+ ai-bot:
360
+ build: .
361
+ container_name: ${multiAppContainerName}
362
+ restart: always
363
+ env_file:
364
+ - ../../.env
365
+ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
366
+ - ${volumeMount}
367
+ ports:
368
+ - "${gatewayPort}:${gatewayPort}"
369
+
370
+ 9router:
371
+ image: node:22-slim
372
+ container_name: ${multiRouterContainerName}
373
+ restart: always
374
+ entrypoint:
375
+ - /bin/sh
376
+ - -c
377
+ - |
378
+ ${indentBlock(docker9RouterEntrypointScript, 8)}
379
+ environment:
380
+ - PORT=${routerPort}
381
+ - HOSTNAME=0.0.0.0
382
+ - CI=true
383
+ volumes:
384
+ - 9router-data:/root/.9router
385
+ - ./sync.js:/tmp/sync.js:ro
386
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
387
+ ports:
388
+ - "${routerPort}:${routerPort}"
389
+
390
+ volumes:
391
+ 9router-data:`;
392
+ } else if (isLocal) {
393
+ const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
394
+ compose = `name: ${multiComposeName}
395
+ services:
396
+ ai-bot:
397
+ build: .
398
+ container_name: ${multiAppContainerName}
399
+ restart: always
400
+ env_file:
401
+ - ../../.env
402
+ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
403
+ - ${volumeMount}
404
+ ports:
405
+ - "${gatewayPort}:${gatewayPort}"
406
+
407
+ ollama:
408
+ image: ollama/ollama:latest
409
+ container_name: ${multiOllamaContainerName}
410
+ restart: always
411
+ environment:
412
+ - OLLAMA_KEEP_ALIVE=24h
413
+ - OLLAMA_NUM_PARALLEL=${multiOllamaNumParallel}
414
+ volumes:
415
+ - ollama-data:/root/.ollama
416
+ entrypoint:
417
+ - /bin/sh
418
+ - -c
419
+ - |
420
+ ollama serve &
421
+ until ollama list > /dev/null 2>&1; do sleep 1; done
422
+ ollama pull ${ollamaModelTag}
423
+ wait
424
+ healthcheck:
425
+ test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
426
+ interval: 10s
427
+ timeout: 5s
428
+ retries: 10
429
+ start_period: 30s
430
+
431
+ volumes:
432
+ ollama-data:`;
433
+ } else {
434
+ compose = `name: ${multiComposeName}
435
+ services:
436
+ ai-bot:
437
+ build: .
438
+ container_name: ${multiAppContainerName}
439
+ restart: always
440
+ env_file:
441
+ - ../../.env
442
+ ${appEnvironmentBlock}${extraHosts} volumes:
443
+ - ${volumeMount}
444
+ ports:
445
+ - "${gatewayPort}:${gatewayPort}"`;
446
+ }
447
+ } else if (is9Router) {
448
+ compose = `name: ${singleComposeName}
449
+ services:
450
+ ai-bot:
451
+ build: .
452
+ container_name: ${singleAppContainerName}
453
+ restart: always
454
+ env_file:
455
+ - ../../.env
456
+ depends_on:
457
+ - 9router
458
+ ${appEnvironmentBlock}${extraHostsBlock}\n volumes:
459
+ - ${volumeMount}
460
+ - openclaw-plugins:/root/project/.openclaw/npm
461
+ ports:
462
+ - "${gatewayPort}:${gatewayPort}"
463
+
464
+ 9router:
465
+ image: node:22-slim
466
+ container_name: ${singleRouterContainerName}
467
+ restart: always
468
+ entrypoint:
469
+ - /bin/sh
470
+ - -c
471
+ - |
472
+ ${indentBlock(docker9RouterEntrypointScript, 8)}
473
+ environment:
474
+ - PORT=${routerPort}
475
+ - HOSTNAME=0.0.0.0
476
+ - CI=true
477
+ volumes:
478
+ - 9router-data:/root/.9router
479
+ - ./sync.js:/tmp/sync.js:ro
480
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
481
+ ports:
482
+ - "${routerPort}:${routerPort}"
483
+
484
+ volumes:
485
+ 9router-data:
486
+ openclaw-plugins:`;
487
+ } else if (isLocal) {
488
+ const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
489
+ compose = `name: ${singleComposeName}
490
+ services:
491
+ ai-bot:
492
+ build: .
493
+ container_name: ${singleAppContainerName}
494
+ restart: always
495
+ env_file: ../../.env
496
+ ${appEnvironmentBlock} depends_on:
497
+ ollama:
498
+ condition: service_healthy
499
+ ${extraHostsBlock}\n ports:
500
+ - "${gatewayPort}:${gatewayPort}"
501
+ volumes:
502
+ - ${volumeMount}
503
+
504
+ ollama:
505
+ image: ollama/ollama:latest
506
+ container_name: ${singleOllamaContainerName}
507
+ restart: always
508
+ environment:
509
+ - OLLAMA_KEEP_ALIVE=24h
510
+ - OLLAMA_NUM_PARALLEL=${singleOllamaNumParallel}
511
+ volumes:
512
+ - ollama-data:/root/.ollama
513
+ entrypoint:
514
+ - /bin/sh
515
+ - -c
516
+ - |
517
+ ollama serve &
518
+ until ollama list > /dev/null 2>&1; do sleep 1; done
519
+ ollama pull ${ollamaModelTag}
520
+ wait
521
+ healthcheck:
522
+ test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
523
+ interval: 10s
524
+ timeout: 5s
525
+ retries: 10
526
+ start_period: 30s
527
+
528
+ volumes:
529
+ ollama-data:`;
530
+ } else {
531
+ compose = `name: ${singleComposeName}
532
+ services:
533
+ ai-bot:
534
+ build: .
535
+ container_name: ${singleAppContainerName}
536
+ restart: always
537
+ env_file:
538
+ - ../../.env
539
+ ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
540
+ - ${volumeMount}
541
+ ports:
542
+ - "${gatewayPort}:${gatewayPort}"`;
543
+ }
544
+
545
+ return {
546
+ dockerfile,
547
+ compose,
548
+ entrypointScript: runtimeScript,
549
+ syncScript,
550
+ patchScript,
551
+ docker9RouterEntrypointScript,
552
+ gatewayPatchCmd: buildGatewayPatchCmd(),
553
+ };
554
+ }
555
+
556
+ root.__openclawDockerGen = {
557
+ encodeBase64Utf8,
558
+ indentBlock,
559
+ build9RouterSmartRouteSyncScript,
560
+ build9RouterPatchScript,
561
+ build9RouterComposeEntrypointScript,
562
+ buildGatewayPatchCmd,
563
+ buildDockerArtifacts,
564
+ };
565
+
566
+ })(typeof globalThis !== 'undefined' ? globalThis : {});
567
+ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
568
+ Object.assign(exports, globalThis.__openclawDockerGen);
569
+ }
570
+
571
+