create-openclaw-bot 5.7.10 → 5.8.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,502 +1,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(dbPath) {
21
- return `const fs=require('fs');const INTERVAL=30000;const p='${dbPath}';
22
- const PM=${JSON.stringify(SMART_ROUTE_PROVIDER_MODELS)};
23
- const PREF=${JSON.stringify(SMART_ROUTE_PROVIDER_ORDER)};
24
- console.log('[sync-combo] 9Router sync loop started...');
25
- const sync = async () => {
26
- try {
27
- let db = {};
28
- try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
29
- if (!db.combos) db.combos = [];
30
- const removeSmartRoute = () => {
31
- const next = db.combos.filter(x => x.id !== 'smart-route');
32
- if (next.length !== db.combos.length) {
33
- db.combos = next;
34
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
35
- console.log('[sync-combo] Removed smart-route (no active providers)');
36
- }
37
- };
38
- const res = await fetch('http://localhost:20128/api/providers');
39
- if (!res.ok) { console.log('[sync-combo] API not ready, retrying...'); return; }
40
- const d = await res.json();
41
- const rawConnections = Array.isArray(d.connections) ? d.connections : Array.isArray(d.providerConnections) ? d.providerConnections : [];
42
- const activeConns = rawConnections.filter(c => c && c.provider && c.isActive !== false && !c.disabled);
43
- const a = [...new Set(activeConns.map(c => c.provider))];
44
- if (!a.length) { console.log('[sync-combo] No active providers reported; keeping existing smart-route'); return; }
45
- a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
46
- const m = [];
47
- for (const pv of a) {
48
- if (PM[pv]) m.push(...PM[pv]);
49
- const conns = activeConns.filter(c => c.provider === pv);
50
- for (const c of conns) {
51
- if (Array.isArray(c.models)) {
52
- for (const mdl of c.models) {
53
- const mdlId = typeof mdl === 'string' ? mdl : mdl.id;
54
- if (mdlId && !m.includes(mdlId) && !m.includes(pv + '/' + mdlId)) {
55
- m.push(pv + '/' + mdlId);
56
- }
57
- }
58
- }
59
- }
60
- }
61
- if (!m.length) { console.log('[sync-combo] No mapped models for active providers; keeping existing smart-route'); return; }
62
- const c = { id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m };
63
- const i = db.combos.findIndex(x => x.id === 'smart-route');
64
- if (i >= 0) {
65
- if (JSON.stringify(db.combos[i].models) !== JSON.stringify(c.models)) {
66
- db.combos[i] = c;
67
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
68
- console.log('[sync-combo] Updated smart-route: ' + c.models.length + ' models');
69
- }
70
- } else {
71
- db.combos.push(c);
72
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
73
- console.log('[sync-combo] Created smart-route: ' + c.models.length + ' models');
74
- }
75
- } catch (e) {}
76
- };
77
- setTimeout(sync, 5000);
78
- setInterval(sync, INTERVAL);`;
79
- }
80
-
81
- function build9RouterPatchScript() {
82
- return `const fs=require('fs');const path=require('path');const cp=require('child_process');
83
- const MODELS=${JSON.stringify(SUPPORTED_CODEX_MODELS.map((model) => model.replace('cx/', '')))};
84
- 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"};
85
- const SELF_TEST_BLOCK=[
86
- 'codex: {',
87
- ' url: "https://chatgpt.com/backend-api/codex/responses",',
88
- ' method: "POST",',
89
- ' authHeader: "Authorization",',
90
- ' authPrefix: "Bearer ",',
91
- ' extraHeaders: { "Content-Type": "application/json", "originator": "codex-cli", "User-Agent": "codex-cli/1.0.18 (macOS; arm64)" },',
92
- ' body: JSON.stringify({',
93
- ' model: "gpt-5.2",',
94
- ' instructions: "You are a coding assistant.",',
95
- ' input: [{ role: "user", content: [{ type: "input_text", text: "Reply with exactly: ok" }] }],',
96
- ' stream: true,',
97
- ' store: false,',
98
- ' }),',
99
- ' acceptStatuses: [200, 400],',
100
- ' refreshable: true,',
101
- ' },'
102
- ].join('\\n');
103
- const roots=new Set();
104
- function add(p){if(p)roots.add(p);}
105
- try{const npmRoot=cp.execSync('npm root -g',{stdio:['ignore','pipe','ignore'],encoding:'utf8'}).trim();if(npmRoot)add(path.join(npmRoot,'9router'));}catch{}
106
- add(path.join(process.env.APPDATA||'','npm','node_modules','9router'));
107
- add('/usr/local/lib/node_modules/9router');
108
- add('/usr/lib/node_modules/9router');
109
- add(path.join(process.cwd(),'node_modules','9router'));
110
- 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;}
111
- function patchText(text,replacers){let next=text;for(const replacer of replacers){next=replacer(next);}return next===text?null:next;}
112
- 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 ],';}));}
113
- function patchCodexLikeFile(filePath){return patchFile(filePath,(text)=>{if(text.includes('max_output_tokens'))return text;return patchText(text,[
114
- (value)=>value.replace(/delete (\\w+)\\.max_tokens,delete \\1\\.user/g,'delete $1.max_tokens,delete $1.max_output_tokens,delete $1.user'),
115
- (value)=>value.replace(/delete (\\w+)\\.max_tokens;(\\s*)delete \\1\\.user/g,'delete $1.max_tokens;$2delete $1.max_output_tokens;$2delete $1.user'),
116
- (value)=>value.replace(' delete body.max_tokens;\\n',' delete body.max_tokens;\\n delete body.max_output_tokens;\\n')
117
- ]);});}
118
- 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;}
119
- 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,[
120
- (value)=>value.replace('let b=a.content.find(a=>"output_text"===a.type);','let b=a.content.find(a=>a&&"output_text"===a.type);'),
121
- (value)=>value.replace('let c=a.content.find(a=>"string"==typeof a.text);','let c=a.content.find(a=>a&&"string"==typeof a.text);'),
122
- (value)=>value.replace('let b=a.filter(a=>a?.type==="message");','let b=a.filter(a=>a&&a?.type==="message");'),
123
- (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));'),
124
- (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){'),
125
- (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;'),
126
- (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):[];'),
127
- (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))'),
128
- (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)),'),
129
- (value)=>value.replace('filter(a=>"function_call"===a.type)','filter(a=>a&&"function_call"===a.type)'),
130
- (value)=>value.replace(/filter\\(a=>"text"===a\\.type\\)/g,'filter(a=>a&&"text"===a.type)'),
131
- (value)=>value.replace(/find\\(a=>"message_stop"===a\\.type\\)/g,'find(a=>a&&"message_stop"===a.type)'),
132
- (value)=>value.replace(/find\\(a=>"content_block_delta"===a\\.type\\)/g,'find(a=>a&&"content_block_delta"===a.type)'),
133
- (value)=>value.replace(/find\\(a=>"message_delta"===a\\.type\\)/g,'find(a=>a&&"message_delta"===a.type)'),
134
- (value)=>value.replace(/find\\(a=>"message_start"===a\\.type\\)/g,'find(a=>a&&"message_start"===a.type)'),
135
- (value)=>value.replace(/for\\(let e of a\\.content\\)(?!if\\(e\\))/g,'for(let e of a.content)if(e)')
136
- ] ))?1:0;}return touched;}
137
- 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);});}
138
- let touched=0;
139
- 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;}
140
- if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}else{console.log('[patch-9router] No compatible 9router source files found to patch.');}`;
141
- }
142
-
143
- function build9RouterComposeEntrypointScript(syncScriptBase64, patchScriptBase64) {
144
- const nineRouterSpec = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon && globalThis.__openclawCommon.NINE_ROUTER_NPM_SPEC) || '9router@latest';
145
- return [
146
- `npm install -g ${nineRouterSpec}`,
147
- `node -e "require('fs').writeFileSync('/tmp/patch-9router.js',Buffer.from('${patchScriptBase64}','base64').toString())"`,
148
- `node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
149
- 'node /tmp/patch-9router.js || true',
150
- 'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
151
- 'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
152
- ].join('\n');
153
- }
154
-
155
- function buildGatewayPatchCmd() {
156
- return `node -e \\"const fs=require('fs'),os=require('os'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://' + entry.address + ':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));}\\"`;
157
- }
158
-
159
- function buildDockerArtifacts(options) {
160
- const {
161
- openClawNpmSpec,
162
- openClawRuntimePackages,
163
- is9Router,
164
- isLocal,
165
- isMultiBot,
166
- hasBrowser,
167
- selectedModel,
168
- agentId,
169
- allSkills = [],
170
- dockerfilePlugins = [],
171
- dockerfileSkillInstallMode = 'none',
172
- runtimeCommandParts = [],
173
- volumeMount = '../../.openclaw:/root/project/.openclaw\\n - ../../:/mnt/project',
174
- singleComposeName = 'oc-bot',
175
- multiComposeName = 'oc-multibot',
176
- singleAppContainerName = 'openclaw-bot',
177
- multiAppContainerName = 'openclaw-multibot',
178
- singleRouterContainerName = '9router',
179
- multiRouterContainerName = '9router-multibot',
180
- singleOllamaContainerName = 'ollama',
181
- multiOllamaContainerName = 'ollama-multibot',
182
- plainSingleExtraHosts = false,
183
- multiOllamaNumParallel = 1,
184
- singleOllamaNumParallel = 1,
185
- emitBrowserInstall = true,
186
-
187
- } = options;
188
-
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
+
189
231
  const browserAptExtra = hasBrowser ? ' xvfb socat' : '';
190
- const browserInstallLines = hasBrowser && emitBrowserInstall
191
- ? [
192
- '',
193
- '# Browser Automation: Playwright engine (needed for native CDP)',
194
- 'RUN npm install -g agent-browser playwright \\',
195
- ' && npx playwright install chromium --with-deps \\',
196
- ' && ln -f -s /root/.cache/ms-playwright/chromium-*/chrome-linux*/chrome /usr/bin/google-chrome',
197
- '',
198
- ''
199
- ].join('\n')
200
- : '';
201
- const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
202
- ? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
203
- : '';
204
- const pluginLines = dockerfilePlugins.length > 0
205
- ? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
206
- : '';
207
- 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);}"`;
208
-
209
- // Dynamic runtime configuration: backup config before any first-run install, restore after.
210
- // Missing plugin install may touch openclaw.json, so preserve critical fields.
211
- const backupConfigScript = `const fs=require('fs'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json'),b=p.replace('openclaw.json','.openclaw-config-backup.json');if(fs.existsSync(p)){fs.copyFileSync(p,b);}`;
212
- const backupConfigB64 = encodeBase64Utf8(backupConfigScript);
213
-
214
- const restoreConfigScript = `const fs=require('fs'),os=require('os'),path=require('path'),p=path.join(process.cwd(),'.openclaw','openclaw.json'),b=p.replace('openclaw.json','.openclaw-config-backup.json');if(fs.existsSync(p)&&fs.existsSync(b)){const c=JSON.parse(fs.readFileSync(p,'utf8'));const bk=JSON.parse(fs.readFileSync(b,'utf8'));const keep=['agents','channels','bindings','commands','models','browser','skills'];for(const k of keep){if(bk[k]&&!c[k])c[k]=bk[k];}const a=new Set(['http://localhost:18791','http://127.0.0.1:18791','http://0.0.0.0:18791']);for(const entries of Object.values(os.networkInterfaces()||{})){for(const entry of entries||[]){if(!entry||entry.internal||entry.family!=='IPv4'||!entry.address)continue;a.add('http://'+entry.address+':18791');}}c.tools=Object.assign({},c.tools,{profile:'full',exec:{host:'gateway',security:'full',ask:'off'}});c.gateway=Object.assign({},c.gateway,{port:18791,bind:'custom',customBindHost:'0.0.0.0',mode:c.gateway?.mode||bk.gateway?.mode||'local',controlUi:Object.assign({},c.gateway?.controlUi,{allowedOrigins:Array.from(a).filter(Boolean)})});fs.writeFileSync(p,JSON.stringify(c,null,2));fs.unlinkSync(b);}`;
215
- const restoreConfigB64 = encodeBase64Utf8(restoreConfigScript);
216
-
217
- const runtimeParts = runtimeCommandParts.filter(Boolean);
218
- const runtimePrelude = [
219
- 'export OPENCLAW_HOME="${OPENCLAW_HOME:-$PWD/.openclaw}"',
220
- 'export OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-$OPENCLAW_HOME}"',
221
- 'mkdir -p "$OPENCLAW_HOME" "$OPENCLAW_STATE_DIR"',
222
- 'if [ "$OPENCLAW_STATE_DIR" != "$OPENCLAW_HOME" ]; then',
223
- ' for path in "$OPENCLAW_HOME"/*; do',
224
- ' [ -e "$path" ] || continue',
225
- ' name="$(basename "$path")"',
226
- ' [ "$name" = "plugin-runtime-deps" ] && continue',
227
- ' [ "$name" = "logs" ] && continue',
228
- ' [ -e "$OPENCLAW_STATE_DIR/$name" ] || ln -s "$path" "$OPENCLAW_STATE_DIR/$name"',
229
- ' done',
230
- 'fi',
231
- 'ensure_plugin() {',
232
- ' id="$1"',
233
- ' spec="$2"',
234
- ' if [ -d "$OPENCLAW_HOME/extensions/$id" ]; then',
235
- ' echo "[entrypoint] plugin $id already installed"',
236
- ' return 0',
237
- ' fi',
238
- ' echo "[entrypoint] plugin $id missing; installing $spec"',
239
- ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
240
- '}',
241
- 'ensure_skill() {',
242
- ' id="$1"',
243
- ' if find "$OPENCLAW_HOME" -maxdepth 4 -type d -path "*/skills/$id" -print -quit 2>/dev/null | grep -q .; then',
244
- ' echo "[entrypoint] skill $id already installed"',
245
- ' return 0',
246
- ' fi',
247
- ' echo "[entrypoint] skill $id missing; installing"',
248
- ' openclaw skills install "$id" 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
249
- '}',
250
- 'echo "[entrypoint] ensuring runtime assets, then starting gateway"',
251
- ];
252
- runtimeParts.unshift(...runtimePrelude);
253
- // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
254
- runtimeParts.unshift(`node -e 'eval(Buffer.from("${backupConfigB64}","base64").toString())'`);
255
- // Restore config AFTER plugin installs (which may clobber openclaw.json)
256
- runtimeParts.push(`node -e 'eval(Buffer.from("${restoreConfigB64}","base64").toString())'`);
257
- if (hasBrowser) {
258
- runtimeParts.push('socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 &');
259
- runtimeParts.push('Xvfb :99 -screen 0 1280x720x24 > /dev/null 2>&1 & DISPLAY=:99 openclaw gateway run');
260
- } else {
261
- runtimeParts.push('openclaw gateway run');
262
- }
263
- const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
264
- const runtimeScriptB64 = encodeBase64Utf8(runtimeScript);
265
- const dockerfile = `FROM node:22-slim
266
-
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
+
267
332
  RUN apt-get update && apt-get install -y git curl python3${browserAptExtra} && rm -rf /var/lib/apt/lists/*
268
- ${browserInstallLines}
269
- ARG OPENCLAW_VER="${openClawNpmSpec}"
270
- ARG CACHE_BUST=""
271
- RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
272
- ${patchLine}
273
- RUN node -e "require('fs').writeFileSync('/usr/local/bin/openclaw-entrypoint.sh', Buffer.from('${runtimeScriptB64}','base64').toString())" && chmod +x /usr/local/bin/openclaw-entrypoint.sh
274
- WORKDIR /root/project
275
-
276
- EXPOSE 18791
277
-
278
- CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
279
-
280
- const syncScript = build9RouterSmartRouteSyncScript('/root/.9router/db.json');
281
- const syncScriptBase64 = encodeBase64Utf8(syncScript);
282
- const patchScript = build9RouterPatchScript();
283
- const patchScriptBase64 = encodeBase64Utf8(patchScript);
284
- const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncScriptBase64, patchScriptBase64);
285
- const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
286
-
287
- const appEnvironmentBlock = ' environment:\n - OPENCLAW_HOME=/root/project/.openclaw\n - OPENCLAW_STATE_DIR=/root/project/.openclaw\n - OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1\n tmpfs:\n - /root/project/.openclaw/plugin-runtime-deps\n';
288
-
289
- let compose;
290
- if (isMultiBot) {
291
- const dependsOn = is9Router
292
- ? ' depends_on:\n - 9router\n'
293
- : isLocal
294
- ? ' depends_on:\n ollama:\n condition: service_healthy\n'
295
- : '';
296
- const extraHosts = hasBrowser ? `${extraHostsBlock}\n` : '';
297
- if (is9Router) {
298
- compose = `name: ${multiComposeName}
299
- services:
300
- ai-bot:
301
- build: .
302
- container_name: ${multiAppContainerName}
303
- restart: always
304
- env_file:
305
- - .env
306
- ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
307
- - ${volumeMount}
308
- ports:
309
- - "18791:18791"
310
-
311
- 9router:
312
- image: node:22-slim
313
- container_name: ${multiRouterContainerName}
314
- restart: always
315
- entrypoint:
316
- - /bin/sh
317
- - -c
318
- - |
319
- ${indentBlock(docker9RouterEntrypointScript, 8)}
320
- environment:
321
- - PORT=20128
322
- - HOSTNAME=0.0.0.0
323
- - CI=true
324
- volumes:
325
- - 9router-data:/root/.9router
326
- ports:
327
- - "20128:20128"
328
-
329
- volumes:
330
- 9router-data:`;
331
- } else if (isLocal) {
332
- const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
333
- compose = `name: ${multiComposeName}
334
- services:
335
- ai-bot:
336
- build: .
337
- container_name: ${multiAppContainerName}
338
- restart: always
339
- env_file:
340
- - .env
341
- ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
342
- - ${volumeMount}
343
- ports:
344
- - "18791:18791"
345
-
346
- ollama:
347
- image: ollama/ollama:latest
348
- container_name: ${multiOllamaContainerName}
349
- restart: always
350
- environment:
351
- - OLLAMA_KEEP_ALIVE=24h
352
- - OLLAMA_NUM_PARALLEL=${multiOllamaNumParallel}
353
- volumes:
354
- - ollama-data:/root/.ollama
355
- entrypoint:
356
- - /bin/sh
357
- - -c
358
- - |
359
- ollama serve &
360
- until ollama list > /dev/null 2>&1; do sleep 1; done
361
- ollama pull ${ollamaModelTag}
362
- wait
363
- healthcheck:
364
- test: ["CMD-SHELL", "ollama list > /dev/null 2>&1"]
365
- interval: 10s
366
- timeout: 5s
367
- retries: 10
368
- start_period: 30s
369
-
370
- volumes:
371
- ollama-data:`;
372
- } else {
373
- compose = `name: ${multiComposeName}
374
- services:
375
- ai-bot:
376
- build: .
377
- container_name: ${multiAppContainerName}
378
- restart: always
379
- env_file:
380
- - .env
381
- ${appEnvironmentBlock}${extraHosts} volumes:
382
- - ${volumeMount}
383
- ports:
384
- - "18791:18791"`;
385
- }
386
- } else if (is9Router) {
387
- compose = `name: ${singleComposeName}
388
- services:
389
- ai-bot:
390
- build: .
391
- container_name: ${singleAppContainerName}
392
- restart: always
393
- env_file:
394
- - .env
395
- depends_on:
396
- - 9router
397
- ${appEnvironmentBlock}${hasBrowser ? `${extraHostsBlock}\n` : ''} volumes:
398
- - ${volumeMount}
399
- ports:
400
- - "18791:18791"
401
-
402
- 9router:
403
- image: node:22-slim
404
- container_name: ${singleRouterContainerName}
405
- restart: always
406
- entrypoint:
407
- - /bin/sh
408
- - -c
409
- - |
410
- ${indentBlock(docker9RouterEntrypointScript, 8)}
411
- environment:
412
- - PORT=20128
413
- - HOSTNAME=0.0.0.0
414
- - CI=true
415
- volumes:
416
- - 9router-data:/root/.9router
417
- ports:
418
- - "20128:20128"
419
-
420
- volumes:
421
- 9router-data:`;
422
- } else if (isLocal) {
423
- const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
424
- compose = `name: ${singleComposeName}
425
- services:
426
- ai-bot:
427
- build: .
428
- container_name: ${singleAppContainerName}
429
- restart: always
430
- env_file: .env
431
- ${appEnvironmentBlock} depends_on:
432
- ollama:
433
- condition: service_healthy
434
- ${hasBrowser ? `${extraHostsBlock}\n` : ''} ports:
435
- - "18791:18791"
436
- volumes:
437
- - ${volumeMount}
438
-
439
- ollama:
440
- image: ollama/ollama:latest
441
- container_name: ${singleOllamaContainerName}
442
- restart: always
443
- environment:
444
- - OLLAMA_KEEP_ALIVE=24h
445
- - OLLAMA_NUM_PARALLEL=${singleOllamaNumParallel}
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: ${singleComposeName}
467
- services:
468
- ai-bot:
469
- build: .
470
- container_name: ${singleAppContainerName}
471
- restart: always
472
- env_file:
473
- - .env
474
- ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
475
- - ${volumeMount}
476
- ports:
477
- - "18791:18791"`;
478
- }
479
-
480
- return {
481
- dockerfile,
482
- compose,
483
- syncScript,
484
- docker9RouterEntrypointScript,
485
- gatewayPatchCmd: buildGatewayPatchCmd(),
486
- };
487
- }
488
-
489
- root.__openclawDockerGen = {
490
- encodeBase64Utf8,
491
- indentBlock,
492
- build9RouterSmartRouteSyncScript,
493
- build9RouterPatchScript,
494
- build9RouterComposeEntrypointScript,
495
- buildGatewayPatchCmd,
496
- buildDockerArtifacts,
497
- };
498
-
499
- })(typeof globalThis !== 'undefined' ? globalThis : {});
500
- if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globalThis.__openclawDockerGen) {
501
- Object.assign(exports, globalThis.__openclawDockerGen);
502
- }
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
+