create-openclaw-bot 5.8.24 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,586 +1,603 @@
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
+ // First-install auto-sync: đăng nhập 9router bằng MẬT KHẨU MẶC ĐỊNH 123456 (9router cấp),
22
+ // gom model của các provider đang active → tạo combo 'smart-route' MỘT LẦN rồi DỪNG.
23
+ // KHÔNG tự tắt Require login nữa (require login giữ ON, pass mặc định 123456, user đổi sau).
24
+ const lines = [
25
+ "const fs = require('fs');",
26
+ "const INTERVAL = 30000;",
27
+ "const MAX_ATTEMPTS = 20; // ~10 phút; tạo combo lần đầu rồi dừng, không lặp vĩnh viễn",
28
+ "const DEFAULT_PASSWORD = '123456'; // mật khẩu mặc định 9router",
29
+ // Docker mounts the DB at /root/.9router; native installs pass NINEROUTER_DB_PATH so the
30
+ // same sync script works on the host without hardcoding the container path.
31
+ "const DB_PATH = process.env.NINEROUTER_DB_PATH || '/root/.9router/db/data.sqlite';",
32
+ "const PORT = process.env.PORT || 20128;",
33
+ "const COMBO_NAME = 'smart-route';",
34
+ "const API_BASE = `http://localhost:${PORT}`;",
35
+ "",
36
+ "// Đăng nhập bằng mật khẩu mặc định → lấy cookie auth_token để gọi API quản trị.",
37
+ "async function login() {",
38
+ " try {",
39
+ " const r = await fetch(`${API_BASE}/api/auth/login`, {",
40
+ " method: 'POST',",
41
+ " headers: { 'Content-Type': 'application/json' },",
42
+ " body: JSON.stringify({ password: DEFAULT_PASSWORD })",
43
+ " });",
44
+ " if (!r.ok) return '';",
45
+ " const sc = r.headers.get('set-cookie') || '';",
46
+ " const m = sc.match(/auth_token=[^;]+/);",
47
+ " return m ? m[0] : '';",
48
+ " } catch (e) { return ''; }",
49
+ "}",
50
+ "",
51
+ "async function trySync() {",
52
+ " if (!fs.existsSync(DB_PATH)) return false;",
53
+ " const cookie = await login(); // mk mặc định 123456; nếu user đã đổi thì login fail",
54
+ " const authHeaders = cookie ? { Cookie: cookie } : {};",
55
+ "",
56
+ " // Combo đã tồn tại? → coi như xong (chỉ tạo lần đầu).",
57
+ " try {",
58
+ " const r = await fetch(`${API_BASE}/api/combos`, { headers: authHeaders });",
59
+ " if (r.status === 401) return false;",
60
+ " const d = await r.json();",
61
+ " if ((d.combos || []).some(c => c.name === COMBO_NAME)) return true;",
62
+ " } catch (e) { return false; }",
63
+ "",
64
+ " // Provider đang active.",
65
+ " let activeProviders = [];",
66
+ " try {",
67
+ " const r = await fetch(`${API_BASE}/api/providers`, { headers: authHeaders });",
68
+ " const d = await r.json();",
69
+ " const conns = d.connections || d.providerConnections || [];",
70
+ " activeProviders = [...new Set(",
71
+ " conns.filter(c => c && c.provider && c.isActive !== false && !c.disabled).map(c => c.provider)",
72
+ " )];",
73
+ " } catch (e) { return false; }",
74
+ " if (!activeProviders.length) return false;",
75
+ "",
76
+ " // Model của provider active (bỏ embedding/image/tts/…).",
77
+ " let models = [];",
78
+ " try {",
79
+ " const r = await fetch(`${API_BASE}/api/models`, { headers: authHeaders });",
80
+ " const d = await r.json();",
81
+ " if (Array.isArray(d.models)) {",
82
+ " models = [...new Set(d.models",
83
+ " .filter(m => activeProviders.includes(m.provider))",
84
+ " .filter(m => !/(embedding|image|tts|stt|audio|vision)/i.test(m.model))",
85
+ " .map(m => m.fullModel))];",
86
+ " }",
87
+ " } catch (e) { return false; }",
88
+ " if (!models.length) return false;",
89
+ "",
90
+ " // Tạo combo smart-route (lần đầu).",
91
+ " try {",
92
+ " await fetch(`${API_BASE}/api/combos`, {",
93
+ " method: 'POST',",
94
+ " headers: { 'Content-Type': 'application/json', ...authHeaders },",
95
+ " body: JSON.stringify({ name: COMBO_NAME, models })",
96
+ " });",
97
+ " console.log('[sync-combo] Created smart-route with ' + models.length + ' models');",
98
+ " return true;",
99
+ " } catch (e) { return false; }",
100
+ "}",
101
+ "",
102
+ "let attempts = 0;",
103
+ "let timer = null;",
104
+ "async function tick() {",
105
+ " attempts++;",
106
+ " let done = false;",
107
+ " try { done = await trySync(); } catch (e) {}",
108
+ " if (done || attempts >= MAX_ATTEMPTS) {",
109
+ " if (timer) clearInterval(timer);",
110
+ " console.log('[sync-combo] finished (attempts=' + attempts + ', done=' + done + ')');",
111
+ " }",
112
+ "}",
113
+ "timer = setInterval(tick, INTERVAL);",
114
+ "setTimeout(tick, 10000);",
115
+ ];
116
+ return lines.join('\n');
117
+ }
118
+
119
+ function build9RouterPatchScript() {
120
+ return `const fs=require('fs');const path=require('path');const cp=require('child_process');
121
+ const MODELS=${JSON.stringify(SUPPORTED_CODEX_MODELS.map((model) => model.replace('cx/', '')))};
122
+ 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"};
123
+ const SELF_TEST_BLOCK=[
124
+ 'codex: {',
125
+ ' url: "https://chatgpt.com/backend-api/codex/responses",',
126
+ ' method: "POST",',
127
+ ' authHeader: "Authorization",',
128
+ ' authPrefix: "Bearer ",',
129
+ ' extraHeaders: { "Content-Type": "application/json", "originator": "codex-cli", "User-Agent": "codex-cli/1.0.18 (macOS; arm64)" },',
130
+ ' body: JSON.stringify({',
131
+ ' model: "gpt-5.2",',
132
+ ' instructions: "You are a coding assistant.",',
133
+ ' input: [{ role: "user", content: [{ type: "input_text", text: "Reply with exactly: ok" }] }],',
134
+ ' stream: true,',
135
+ ' store: false,',
136
+ ' }),',
137
+ ' acceptStatuses: [200, 400],',
138
+ ' refreshable: true,',
139
+ ' },'
140
+ ].join('\\n');
141
+ const roots=new Set();
142
+ function add(p){if(p)roots.add(p);}
143
+ try{const npmRoot=cp.execSync('npm root -g',{stdio:['ignore','pipe','ignore'],encoding:'utf8'}).trim();if(npmRoot)add(path.join(npmRoot,'9router'));}catch{}
144
+ add(path.join(process.env.APPDATA||'','npm','node_modules','9router'));
145
+ add('/usr/local/lib/node_modules/9router');
146
+ add('/usr/lib/node_modules/9router');
147
+ add(path.join(process.cwd(),'node_modules','9router'));
148
+ 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;}
149
+ function patchText(text,replacers){let next=text;for(const replacer of replacers){next=replacer(next);}return next===text?null:next;}
150
+ 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 ],';}));}
151
+ function patchCodexLikeFile(filePath){return patchFile(filePath,(text)=>{if(text.includes('max_output_tokens'))return text;return patchText(text,[
152
+ (value)=>value.replace(/delete (\\w+)\\.max_tokens,delete \\1\\.user/g,'delete $1.max_tokens,delete $1.max_output_tokens,delete $1.user'),
153
+ (value)=>value.replace(/delete (\\w+)\\.max_tokens;(\\s*)delete \\1\\.user/g,'delete $1.max_tokens;$2delete $1.max_output_tokens;$2delete $1.user'),
154
+ (value)=>value.replace(' delete body.max_tokens;\\n',' delete body.max_tokens;\\n delete body.max_output_tokens;\\n')
155
+ ]);});}
156
+ 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;}
157
+ 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,[
158
+ (value)=>value.replace('let b=a.content.find(a=>"output_text"===a.type);','let b=a.content.find(a=>a&&"output_text"===a.type);'),
159
+ (value)=>value.replace('let c=a.content.find(a=>"string"==typeof a.text);','let c=a.content.find(a=>a&&"string"==typeof a.text);'),
160
+ (value)=>value.replace('let b=a.filter(a=>a?.type==="message");','let b=a.filter(a=>a&&a?.type==="message");'),
161
+ (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));'),
162
+ (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){'),
163
+ (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;'),
164
+ (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):[];'),
165
+ (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))'),
166
+ (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)),'),
167
+ (value)=>value.replace('filter(a=>"function_call"===a.type)','filter(a=>a&&"function_call"===a.type)'),
168
+ (value)=>value.replace(/filter\\(a=>"text"===a\\.type\\)/g,'filter(a=>a&&"text"===a.type)'),
169
+ (value)=>value.replace(/find\\(a=>"message_stop"===a\\.type\\)/g,'find(a=>a&&"message_stop"===a.type)'),
170
+ (value)=>value.replace(/find\\(a=>"content_block_delta"===a\\.type\\)/g,'find(a=>a&&"content_block_delta"===a.type)'),
171
+ (value)=>value.replace(/find\\(a=>"message_delta"===a\\.type\\)/g,'find(a=>a&&"message_delta"===a.type)'),
172
+ (value)=>value.replace(/find\\(a=>"message_start"===a\\.type\\)/g,'find(a=>a&&"message_start"===a.type)'),
173
+ (value)=>value.replace(/for\\(let e of a\\.content\\)(?!if\\(e\\))/g,'for(let e of a.content)if(e)')
174
+ ] ))?1:0;}return touched;}
175
+ 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);});}
176
+ let touched=0;
177
+ 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;}
178
+ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}else{console.log('[patch-9router] No compatible 9router source files found to patch.');}`;
179
+ }
180
+
181
+ function build9RouterComposeEntrypointScript(routerPort) {
182
+ const port = routerPort || 20128;
183
+ const nineRouterSpec = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon && globalThis.__openclawCommon.NINE_ROUTER_NPM_SPEC) || '9router@latest';
184
+ return [
185
+ `npm install -g ` + nineRouterSpec,
186
+ 'node /tmp/patch-9router.js || true',
187
+ '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:true})); } db.close(); }catch(e){}" || true',
188
+ 'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
189
+ `exec 9router -n -l -H 0.0.0.0 -p ${port} --skip-update`
190
+ ].join('\n');
191
+ }
192
+
193
+ function buildGatewayPatchCmd() {
194
+ 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));}\\"`;
195
+ }
196
+
197
+ function buildDockerArtifacts(options) {
198
+ const {
199
+ openClawNpmSpec,
200
+ openClawRuntimePackages,
201
+ is9Router,
202
+ isLocal,
203
+ isMultiBot,
204
+ hasBrowser = false,
205
+ selectedModel,
206
+ agentId,
207
+ allSkills = [],
208
+ dockerfilePlugins = [],
209
+ dockerfileSkillInstallMode = 'none',
210
+ runtimeCommandParts = [],
211
+ volumeMount = '../../.openclaw:/home/node/project/.openclaw\n - ../../:/mnt/project',
212
+ singleComposeName = 'oc-bot',
213
+ multiComposeName = 'oc-multibot',
214
+ singleAppContainerName = 'openclaw-bot',
215
+ multiAppContainerName = 'openclaw-multibot',
216
+ singleRouterContainerName = '9router',
217
+ multiRouterContainerName = '9router-multibot',
218
+ singleOllamaContainerName = 'ollama',
219
+ multiOllamaContainerName = 'ollama-multibot',
220
+ plainSingleExtraHosts = false,
221
+ multiOllamaNumParallel = 1,
222
+ singleOllamaNumParallel = 1,
223
+ gatewayPort = 18789,
224
+ routerPort = 20128,
225
+ osChoice = '',
226
+ } = options;
227
+ // Windows bind-mounts give ClawHub-installed plugins world-writable perms (which openclaw
228
+ // blocks), so on Windows we isolate extensions in a named volume. On macOS/Linux bind-mounts
229
+ // are fine, so keep extensions under the .openclaw bind mount → plugins stay visible/synced
230
+ // on the host (e.g. you can see zalo-mod in .openclaw/extensions).
231
+ const useExtensionsVolume = osChoice === 'win';
232
+ const extVolMount = useExtensionsVolume ? '\n - openclaw-extensions:/home/node/project/.openclaw/extensions' : '';
233
+ const extVolDecl = useExtensionsVolume ? '\n openclaw-extensions:' : '';
234
+ const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
235
+ ? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
236
+ : '';
237
+ const pluginLines = dockerfilePlugins.length > 0
238
+ ? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
239
+ : '';
240
+ 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);}"`;
241
+
242
+ // Dynamic runtime configuration: backup config before any first-run install, restore after.
243
+ // Missing plugin install may touch openclaw.json, so preserve critical fields.
244
+ 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);}`;
245
+
246
+ 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);}`;
247
+ 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));}}`;
248
+
249
+ const runtimeParts = runtimeCommandParts.filter(Boolean);
250
+ const runtimePrelude = [
251
+ 'export OPENCLAW_HOME="${OPENCLAW_HOME:-$PWD/.openclaw}"',
252
+ 'export OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"',
253
+ 'mkdir -p "$OPENCLAW_HOME" "$OPENCLAW_STATE_DIR"',
254
+ 'if [ "$OPENCLAW_STATE_DIR" != "$OPENCLAW_HOME" ]; then',
255
+ ' for path in "$OPENCLAW_HOME"/*; do',
256
+ ' [ -e "$path" ] || continue',
257
+ ' name="$(basename "$path")"',
258
+ ' [ "$name" = "plugin-runtime-deps" ] && continue',
259
+ ' [ "$name" = "logs" ] && continue',
260
+ ' [ -e "$OPENCLAW_STATE_DIR/$name" ] || ln -s "$path" "$OPENCLAW_STATE_DIR/$name"',
261
+ ' done',
262
+ 'fi',
263
+ 'ensure_plugin() {',
264
+ ' id="$1"',
265
+ ' spec="$2"',
266
+ ' if [ -d "$OPENCLAW_HOME/extensions/$id" ]; then',
267
+ ' echo "[entrypoint] plugin $id already installed"',
268
+ ' return 0',
269
+ ' fi',
270
+ ' echo "[entrypoint] plugin $id missing; installing $spec"',
271
+ ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
272
+ '}',
273
+ 'ensure_zalouser() {',
274
+ ' NPM_DIR="$OPENCLAW_HOME/npm"',
275
+ ' PKG_DIR="$NPM_DIR/node_modules/@openclaw/zalouser"',
276
+ ' if [ -d "$PKG_DIR" ] || [ -d "$OPENCLAW_HOME/extensions/zalouser" ]; then',
277
+ ' echo "[entrypoint] zalouser plugin already installed (npm or extensions) — skipping"',
278
+ ' else',
279
+ ' echo "[entrypoint] zalouser plugin missing; installing via npm"',
280
+ ' mkdir -p "$NPM_DIR"',
281
+ ' cd "$NPM_DIR"',
282
+ ' npm init -y 2>/dev/null || true',
283
+ ' npm install @openclaw/zalouser@latest 2>/dev/null || echo "[entrypoint] warning: failed to install @openclaw/zalouser"',
284
+ ' cd /home/node/project',
285
+ ' fi',
286
+ '}',
287
+ 'ensure_skill() {',
288
+ ' id="$1"',
289
+ ' if find "$OPENCLAW_HOME" -maxdepth 4 -type d -path "*/skills/$id" -print -quit 2>/dev/null | grep -q .; then',
290
+ ' echo "[entrypoint] skill $id already installed"',
291
+ ' return 0',
292
+ ' fi',
293
+ ' echo "[entrypoint] skill $id missing; installing"',
294
+ ' openclaw skills install "$id" 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
295
+ '}',
296
+ 'echo "[entrypoint] ensuring runtime assets, then starting gateway"',
297
+ ];
298
+ runtimeParts.unshift(...runtimePrelude);
299
+ // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
300
+ runtimeParts.unshift(`node - <<'NODE'\n${backupConfigScript}\nNODE`);
301
+ // Restore config AFTER plugin installs (which may clobber openclaw.json)
302
+ runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
303
+ runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
304
+ // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
305
+ runtimeParts.push([
306
+ '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
307
+ 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
308
+ 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
309
+ ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
310
+ ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
311
+ 'fi',
312
+ '# Automatically run Zalo sticker-mention patch if skill is present',
313
+ 'for MENTIONS_JS in $(find "$OPENCLAW_HOME" -maxdepth 5 -path "*/skills/*/mentions.js" -type f 2>/dev/null); do',
314
+ ' if [ -f "$MENTIONS_JS" ]; then',
315
+ ' echo "[entrypoint] Running patch: $MENTIONS_JS"',
316
+ ' node "$MENTIONS_JS" || echo "[entrypoint] Warning: failed to run patch $MENTIONS_JS"',
317
+ ' fi',
318
+ 'done',
319
+ ].join('\n'));
320
+ // Expose zalouser's ZCA API map (globalThis.__zcaApiByProfile) BEFORE the gateway imports
321
+ // zalouser, so openclaw-zalo-mod can reach the live Zalo API (dashboard Sync Account, group
322
+ // admin lookups, …). zalo-mod's own runtime patch lands AFTER zalouser is already imported,
323
+ // so it never takes effect on the running module — patching here (post sticker-mention restore,
324
+ // pre gateway-run) is the reliable fix.
325
+ {
326
+ const exposeZcaScript = [
327
+ "const fs=require('fs'),cp=require('child_process');",
328
+ "let files=[];",
329
+ "try{ files=cp.execSync('find \"'+(process.env.OPENCLAW_HOME||'')+'/npm\" -path \"*/@openclaw/zalouser/dist/zalo-js-*.js\" -type f 2>/dev/null',{encoding:'utf8'}).split('\\n').filter(Boolean); }catch(e){}",
330
+ "const reps=[['const apiByProfile = /* @__PURE__ */ new Map();','const apiByProfile = globalThis.__zcaApiByProfile || (globalThis.__zcaApiByProfile = /* @__PURE__ */ new Map());'],['const apiByProfile = new Map();','const apiByProfile = globalThis.__zcaApiByProfile || (globalThis.__zcaApiByProfile = new Map());']];",
331
+ "for(const f of files){ try{ let s=fs.readFileSync(f,'utf8'); if(s.includes('globalThis.__zcaApiByProfile')) continue; for(const pair of reps){ if(s.includes(pair[0])){ s=s.replace(pair[0],pair[1]); fs.writeFileSync(f,s); console.log('[entrypoint] exposed __zcaApiByProfile for zalo-mod in '+f); break; } } }catch(e){} }",
332
+ ].join('\n');
333
+ runtimeParts.push(`# Expose zalouser ZCA API map for openclaw-zalo-mod (before gateway imports zalouser)\nnode - <<'NODE'\n${exposeZcaScript}\nNODE`);
334
+ }
335
+ runtimeParts.push([
336
+ '# Zalo channel auto-restart monitor (background)',
337
+ '(',
338
+ ' sleep 180',
339
+ ' while true; do',
340
+ ' sleep 60',
341
+ ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
342
+ ' if echo "$STATUS" | grep -qi "stopped"; then',
343
+ ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
344
+ ' sleep 5',
345
+ ' kill 1 2>/dev/null || true',
346
+ ' fi',
347
+ ' done',
348
+ ') &',
349
+ ].join('\n'));
350
+ runtimeParts.push('openclaw gateway run');
351
+ const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
352
+ let browserInstall = '';
353
+ if (hasBrowser) {
354
+ browserInstall = '\n# Install browser and system dependencies for Playwright\nRUN npx playwright install-deps chromium && npx playwright install chromium\n';
355
+ }
356
+ const dockerfile = `FROM node:22-slim
357
+
358
+ RUN apt-get update && apt-get install -y git curl python3 && rm -rf /var/lib/apt/lists/*
359
+
360
+ ARG OPENCLAW_VER="${openClawNpmSpec}"
361
+ ARG CACHE_BUST=""
362
+ RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
363
+ ${patchLine}${browserInstall}
364
+
365
+ COPY entrypoint.sh /usr/local/bin/openclaw-entrypoint.sh
366
+ RUN chmod +x /usr/local/bin/openclaw-entrypoint.sh
367
+ WORKDIR /home/node/project
368
+
369
+ EXPOSE ${gatewayPort}
370
+
371
+ CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
372
+
373
+ const syncScript = build9RouterSmartRouteSyncScript();
374
+ const patchScript = build9RouterPatchScript();
375
+ const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
376
+ const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
377
+
378
+ 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`;
379
+
380
+ let compose;
381
+ if (isMultiBot) {
382
+ const dependsOn = is9Router
383
+ ? ' depends_on:\n - 9router\n'
384
+ : isLocal
385
+ ? ' depends_on:\n ollama:\n condition: service_healthy\n'
386
+ : '';
387
+ const extraHosts = `${extraHostsBlock}\n`;
388
+ if (is9Router) {
389
+ compose = `name: ${multiComposeName}
390
+ services:
391
+ ai-bot:
392
+ build: .
393
+ container_name: ${multiAppContainerName}
394
+ restart: always
395
+ env_file:
396
+ - ../../.env
397
+ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
398
+ - ${volumeMount}
399
+ ports:
400
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"
401
+
402
+ 9router:
403
+ image: node:22-slim
404
+ container_name: ${multiRouterContainerName}
405
+ restart: always
406
+ entrypoint:
407
+ - /bin/sh
408
+ - -c
409
+ - |
410
+ ${indentBlock(docker9RouterEntrypointScript, 8)}
411
+ environment:
412
+ - PORT=${routerPort}
413
+ - HOSTNAME=0.0.0.0
414
+ - CI=true
415
+ volumes:
416
+ - 9router-data:/root/.9router
417
+ - ./sync.js:/tmp/sync.js:ro
418
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
419
+ ports:
420
+ - "127.0.0.1:${routerPort}:${routerPort}"
421
+
422
+ volumes:
423
+ 9router-data:`;
424
+ } else if (isLocal) {
425
+ const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
426
+ compose = `name: ${multiComposeName}
427
+ services:
428
+ ai-bot:
429
+ build: .
430
+ container_name: ${multiAppContainerName}
431
+ restart: always
432
+ env_file:
433
+ - ../../.env
434
+ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
435
+ - ${volumeMount}
436
+ ports:
437
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"
438
+
439
+ ollama:
440
+ image: ollama/ollama:latest
441
+ container_name: ${multiOllamaContainerName}
442
+ restart: always
443
+ environment:
444
+ - OLLAMA_KEEP_ALIVE=24h
445
+ - OLLAMA_NUM_PARALLEL=${multiOllamaNumParallel}
446
+ volumes:
447
+ - ollama-data:/root/.ollama
448
+ entrypoint:
449
+ - /bin/sh
450
+ - -c
451
+ - |
452
+ ollama serve &
453
+ until ollama list > /dev/null 2>&1; do sleep 1; done
454
+ ollama pull ${ollamaModelTag}
455
+ wait
456
+ healthcheck:
457
+ test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
458
+ interval: 10s
459
+ timeout: 5s
460
+ retries: 10
461
+ start_period: 30s
462
+
463
+ volumes:
464
+ ollama-data:`;
465
+ } else {
466
+ compose = `name: ${multiComposeName}
467
+ services:
468
+ ai-bot:
469
+ build: .
470
+ container_name: ${multiAppContainerName}
471
+ restart: always
472
+ env_file:
473
+ - ../../.env
474
+ ${appEnvironmentBlock}${extraHosts} volumes:
475
+ - ${volumeMount}
476
+ ports:
477
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"`;
478
+ }
479
+ } else if (is9Router) {
480
+ compose = `name: ${singleComposeName}
481
+ services:
482
+ ai-bot:
483
+ build: .
484
+ container_name: ${singleAppContainerName}
485
+ restart: always
486
+ env_file:
487
+ - ../../.env
488
+ depends_on:
489
+ - 9router
490
+ ${appEnvironmentBlock}${extraHostsBlock}\n volumes:
491
+ - ${volumeMount}
492
+ - openclaw-plugins:/home/node/project/.openclaw/npm${extVolMount}
493
+ ports:
494
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"
495
+
496
+ 9router:
497
+ image: node:22-slim
498
+ container_name: ${singleRouterContainerName}
499
+ restart: always
500
+ entrypoint:
501
+ - /bin/sh
502
+ - -c
503
+ - |
504
+ ${indentBlock(docker9RouterEntrypointScript, 8)}
505
+ environment:
506
+ - PORT=${routerPort}
507
+ - HOSTNAME=0.0.0.0
508
+ - CI=true
509
+ volumes:
510
+ - 9router-data:/root/.9router
511
+ - ./sync.js:/tmp/sync.js:ro
512
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
513
+ ports:
514
+ - "127.0.0.1:${routerPort}:${routerPort}"
515
+
516
+ volumes:
517
+ 9router-data:
518
+ openclaw-plugins:${extVolDecl}`;
519
+ } else if (isLocal) {
520
+ const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
521
+ compose = `name: ${singleComposeName}
522
+ services:
523
+ ai-bot:
524
+ build: .
525
+ container_name: ${singleAppContainerName}
526
+ restart: always
527
+ env_file: ../../.env
528
+ ${appEnvironmentBlock} depends_on:
529
+ ollama:
530
+ condition: service_healthy
531
+ ${extraHostsBlock}\n ports:
532
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"
533
+ volumes:
534
+ - ${volumeMount}
535
+
536
+ ollama:
537
+ image: ollama/ollama:latest
538
+ container_name: ${singleOllamaContainerName}
539
+ restart: always
540
+ environment:
541
+ - OLLAMA_KEEP_ALIVE=24h
542
+ - OLLAMA_NUM_PARALLEL=${singleOllamaNumParallel}
543
+ volumes:
544
+ - ollama-data:/root/.ollama
545
+ entrypoint:
546
+ - /bin/sh
547
+ - -c
548
+ - |
549
+ ollama serve &
550
+ until ollama list > /dev/null 2>&1; do sleep 1; done
551
+ ollama pull ${ollamaModelTag}
552
+ wait
553
+ healthcheck:
554
+ test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
555
+ interval: 10s
556
+ timeout: 5s
557
+ retries: 10
558
+ start_period: 30s
559
+
560
+ volumes:
561
+ ollama-data:`;
562
+ } else {
563
+ compose = `name: ${singleComposeName}
564
+ services:
565
+ ai-bot:
566
+ build: .
567
+ container_name: ${singleAppContainerName}
568
+ restart: always
569
+ env_file:
570
+ - ../../.env
571
+ ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
572
+ - ${volumeMount}
573
+ ports:
574
+ - "127.0.0.1:${gatewayPort}:${gatewayPort}"`;
575
+ }
576
+
577
+ return {
578
+ dockerfile,
579
+ compose,
580
+ entrypointScript: runtimeScript,
581
+ syncScript,
582
+ patchScript,
583
+ docker9RouterEntrypointScript,
584
+ gatewayPatchCmd: buildGatewayPatchCmd(),
585
+ };
586
+ }
587
+
588
+ root.__openclawDockerGen = {
589
+ encodeBase64Utf8,
590
+ indentBlock,
591
+ build9RouterSmartRouteSyncScript,
592
+ build9RouterPatchScript,
593
+ build9RouterComposeEntrypointScript,
594
+ buildGatewayPatchCmd,
595
+ buildDockerArtifacts,
596
+ };
597
+
598
+ })(typeof globalThis !== 'undefined' ? globalThis : {});
599
+ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
600
+ Object.assign(exports, globalThis.__openclawDockerGen);
601
+ }
602
+
603
+