create-openclaw-bot 5.7.9 → 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.
package/dist/setup.js CHANGED
@@ -1,13 +1,13 @@
1
1
  /* ============================================
2
- OpenClaw Setup Wizard — Logic v2
2
+ OpenClaw Setup Wizard — Logic v2
3
3
  Multi-model, Multi-plugin, Multi-channel
4
4
  ============================================ */
5
- // AUTO-GENERATED by build.mjs — edit files in src/setup/ instead
5
+ // AUTO-GENERATED by build.mjs — edit files in src/setup/ instead
6
6
 
7
7
  (function () {
8
8
  'use strict';
9
9
 
10
- // ── Globals: CDN logos, state, shared utils (setup/data/header.js) ─
10
+ // ── Globals: CDN logos, state, shared utils (setup/data/header.js) ─
11
11
  // @ts-nocheck
12
12
  /* eslint-disable no-undef, no-unused-vars */
13
13
  /**
@@ -76,7 +76,7 @@
76
76
  || 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
77
77
 
78
78
  function getGatewayAllowedOrigins(port) {
79
- const normalizedPort = Number(port) || 18791;
79
+ const normalizedPort = Number(port) || 18789;
80
80
  const origins = new Set([
81
81
  `http://localhost:${normalizedPort}`,
82
82
  `http://127.0.0.1:${normalizedPort}`,
@@ -89,7 +89,7 @@
89
89
  return Array.from(origins);
90
90
  }
91
91
 
92
- // ── PROVIDERS object (setup/data/providers.js) ─────────────────────
92
+ // ── PROVIDERS object (setup/data/providers.js) ─────────────────────
93
93
  // @ts-nocheck
94
94
  /* eslint-disable no-undef, no-unused-vars */
95
95
  /**
@@ -212,7 +212,7 @@
212
212
  };
213
213
 
214
214
 
215
- // ── CHANNELS, system prompts, security rules (setup/data/channels.js)
215
+ // ── CHANNELS, system prompts, security rules (setup/data/channels.js)
216
216
  // @ts-nocheck
217
217
  /* eslint-disable no-undef, no-unused-vars */
218
218
  /**
@@ -383,7 +383,7 @@
383
383
  - ✅ Limit exposed ports (only 38789)`,
384
384
  };
385
385
 
386
- // ── PLUGINS list (setup/data/plugins.js) ───────────────────────────
386
+ // ── PLUGINS list (setup/data/plugins.js) ───────────────────────────
387
387
  // @ts-nocheck
388
388
  /* eslint-disable no-undef, no-unused-vars */
389
389
  /**
@@ -445,7 +445,7 @@
445
445
  ];
446
446
 
447
447
 
448
- // ── SKILLS list (setup/data/skills.js) ─────────────────────────────
448
+ // ── SKILLS list (setup/data/skills.js) ─────────────────────────────
449
449
  // @ts-nocheck
450
450
  /* eslint-disable no-undef, no-unused-vars */
451
451
  /**
@@ -616,10 +616,10 @@
616
616
  }
617
617
 
618
618
 
619
- // ── Shared runtime constants, relay helpers, auth profile builders (setup/shared/common-gen.js)
619
+ // ── Shared runtime constants, relay helpers, auth profile builders (setup/shared/common-gen.js)
620
620
  // @ts-nocheck
621
621
  (function (root) {
622
- const OPENCLAW_NPM_SPEC = 'openclaw@2026.5.4';
622
+ const OPENCLAW_NPM_SPEC = 'openclaw@latest';
623
623
  const OPENCLAW_RUNTIME_PACKAGES = 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
624
624
  const NINE_ROUTER_NPM_SPEC = '9router@latest';
625
625
  const NINE_ROUTER_PORT = 20128;
@@ -861,8 +861,9 @@ If setup reported a plugin install error, run this after the bot is running:
861
861
  return JSON.stringify(buildAuthProfilesJson(options), null, 2);
862
862
  }
863
863
 
864
- function get9RouterBaseUrl(deployMode = 'native') {
865
- return deployMode === 'docker' ? `${NINE_ROUTER_DOCKER_API_BASE_URL}/v1` : `${NINE_ROUTER_API_BASE_URL}/v1`;
864
+ function get9RouterBaseUrl(deployMode = 'native', routerPort) {
865
+ const port = routerPort || NINE_ROUTER_PORT;
866
+ return deployMode === 'docker' ? `http://9router:${port}/v1` : `http://localhost:${port}/v1`;
866
867
  }
867
868
 
868
869
  function build9RouterProviderConfig(baseUrl = `${NINE_ROUTER_API_BASE_URL}/v1`) {
@@ -870,6 +871,9 @@ If setup reported a plugin install error, run this after the bot is running:
870
871
  baseUrl,
871
872
  apiKey: NINE_ROUTER_PROXY_API_KEY,
872
873
  api: 'openai-completions',
874
+ request: {
875
+ allowPrivateNetwork: true,
876
+ },
873
877
  models: [
874
878
  {
875
879
  id: 'smart-route',
@@ -877,18 +881,12 @@ If setup reported a plugin install error, run this after the bot is running:
877
881
  contextWindow: 200000,
878
882
  maxTokens: 8192,
879
883
  },
880
- ...SUPPORTED_CODEX_MODELS.map((id) => ({
881
- id,
882
- name: `Codex ${id.slice(3).replace(/-/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase())}`,
883
- contextWindow: 200000,
884
- maxTokens: 8192,
885
- })),
886
884
  ],
887
885
  };
888
886
  }
889
887
 
890
- function buildGatewayConfig(port = 18791, deployMode = 'native', allowedOrigins = [], osChoice = '') {
891
- const normalizedPort = Number(port) || 18791;
888
+ function buildGatewayConfig(port = 18789, deployMode = 'native', allowedOrigins = [], osChoice = '') {
889
+ const normalizedPort = Number(port) || 18789;
892
890
  const cfg = {
893
891
  port: normalizedPort,
894
892
  mode: 'local',
@@ -933,7 +931,7 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
933
931
  Object.assign(exports, globalThis.__openclawCommon);
934
932
  }
935
933
 
936
- // ── Shared workspace file builders (IDENTITY, SOUL, AGENTS, TOOLS, TEAMS...) (setup/shared/workspace-gen.js)
934
+ // ── Shared workspace file builders (IDENTITY, SOUL, AGENTS, TOOLS, TEAMS...) (setup/shared/workspace-gen.js)
937
935
  /** @typedef {typeof globalThis & { __openclawWorkspace?: Record<string, Function> }} OpenClawWorkspaceRoot */
938
936
 
939
937
  const workspaceRoot = /** @type {OpenClawWorkspaceRoot} */ (
@@ -1185,19 +1183,48 @@ const CDP_URL = 'http://127.0.0.1:9222';
1185
1183
 
1186
1184
  function buildBrowserDoc(options = {}) {
1187
1185
  const { isVi = true, variant = 'wizard', workspaceRoot = '' } = options;
1188
- // Normalize: strip trailing slash so path joins work cleanly
1189
1186
  const wsRoot = workspaceRoot.replace(/\/+$/, '');
1190
1187
  const btPath = wsRoot ? `${wsRoot}/browser-tool.js` : 'browser-tool.js';
1191
-
1192
- if (variant === 'cli-desktop') {
1193
- return `# Browser Automation (Desktop Mode)\n\nBot controls your actual Chrome on screen through Chrome Debug at \`http://127.0.0.1:9222\`. Every action is visible.\n\n## Usage (v2)\n\`\`\`bash\n# Navigation\nnode ${btPath} status\nnode ${btPath} open "https://google.com"\nnode ${btPath} get_url\n\n# Content extraction\nnode ${btPath} get_text\nnode ${btPath} get_text 8000 # custom max length\nnode ${btPath} get_links # all links\nnode ${btPath} get_links "/posts/" # filtered\nnode ${btPath} get_posts # Facebook group posts w/ permalinks\nnode ${btPath} evaluate "document.title"\nnode ${btPath} console\n\n# Screenshots & export\nnode ${btPath} screenshot\nnode ${btPath} screenshot_full\nnode ${btPath} pdf\n\n# Interactions\nnode ${btPath} click "button.submit"\nnode ${btPath} fill "input[name='q']" "search"\nnode ${btPath} press "Enter"\nnode ${btPath} hover "a.link"\nnode ${btPath} select "select#country" "VN"\nnode ${btPath} upload "input[type=file]" "/tmp/photo.jpg"\n\n# Scrolling & viewport\nnode ${btPath} scroll\nnode ${btPath} scroll 1500\nnode ${btPath} wait 3000\nnode ${btPath} resize 1920 1080\n\n# Tab management\nnode ${btPath} tabs\nnode ${btPath} new_tab "https://example.com"\nnode ${btPath} switch_tab 1\nnode ${btPath} close_tab 2\n\`\`\`\n\n## MANDATORY RULES\n- NEVER refuse to open the browser when user asks.\n- In Desktop mode, always target the \`host-chrome\` / Chrome Debug session first.\n- If Chrome Debug is unreachable or returns \`ECONNREFUSED\`, tell user to run \`start-chrome-debug.bat\` again.\n- Use \`get_posts\` instead of \`get_text\` when scraping Facebook — it extracts permalinks.\n`;
1194
- }
1195
- if (variant === 'cli-server') {
1196
- return `# Browser Automation (Headless Server Mode)\n\nBot uses a headless Chromium instance running inside the Docker container. No GUI needed!\n\n## Notes\n- Running on Ubuntu Server / VPS (no GUI required)\n- Uses Playwright + Headless Chromium installed inside Docker\n- For Cloudflare bypass, switch to Desktop mode (requires Windows/Mac with Chrome)\n`;
1197
- }
1198
- return isVi
1199
- ? `# Browser Automation\n\nDùng file \`browser-tool.js\` để điều khiển Chrome debug tại \`http://127.0.0.1:9222\`.\nScript: \`${btPath}\`\nPhiên bản v2 hỗ trợ: open, get_text, get_links, get_posts, evaluate, screenshot, pdf, click, fill, press, hover, select, upload, scroll, tabs, và nhiều lệnh khác.`
1200
- : `# Browser Automation\n\nUse \`browser-tool.js\` to control Chrome debug on \`http://127.0.0.1:9222\`.\nScript: \`${btPath}\`\nVersion v2 supports: open, get_text, get_links, get_posts, evaluate, screenshot, pdf, click, fill, press, hover, select, upload, scroll, tabs, and more.`;
1188
+ const modeHeading = variant === 'cli-server' ? '# Headless Server Mode\n\n' : '';
1189
+
1190
+ return `${modeHeading}# Navigation
1191
+ node ${btPath} status
1192
+ node ${btPath} open "https://google.com"
1193
+ node ${btPath} get_url
1194
+
1195
+ # ⭐ Content extraction — LUÔN dùng get_posts cho Facebook
1196
+ node ${btPath} get_posts
1197
+ node ${btPath} get_text
1198
+ node ${btPath} get_text 8000
1199
+ node ${btPath} get_links
1200
+ node ${btPath} get_links "/posts/"
1201
+ node ${btPath} evaluate "document.title"
1202
+ node ${btPath} console
1203
+
1204
+ # Screenshots & export
1205
+ node ${btPath} screenshot
1206
+ node ${btPath} screenshot_full
1207
+ node ${btPath} pdf
1208
+
1209
+ # Interactions
1210
+ node ${btPath} click "button.submit"
1211
+ node ${btPath} fill "input[name='q']" "search"
1212
+ node ${btPath} press "Enter"
1213
+ node ${btPath} hover "a.link"
1214
+ node ${btPath} select "select#id" "value"
1215
+ node ${btPath} upload "input[type=file]" "/tmp/photo.jpg"
1216
+
1217
+ # Scrolling & viewport
1218
+ node ${btPath} scroll
1219
+ node ${btPath} scroll 1500
1220
+ node ${btPath} wait 3000
1221
+ node ${btPath} resize 1920 1080
1222
+
1223
+ # Tab management
1224
+ node ${btPath} tabs
1225
+ node ${btPath} new_tab "https://example.com"
1226
+ node ${btPath} switch_tab 1
1227
+ node ${btPath} close_tab 2`;
1201
1228
  }
1202
1229
 
1203
1230
  function buildSecurityRules(isVi = true) {
@@ -1235,14 +1262,14 @@ const CDP_URL = 'http://127.0.0.1:9222';
1235
1262
  ? '- If metadata does not clearly say this is a group/supergroup, treat it as a private DM and reply normally.\n'
1236
1263
  : '';
1237
1264
  return isVi
1238
- ? `# Hướng dẫn vận hành\n\n## Vai trò\nBạn là **${botName}**, ${botDesc ? botDesc.toLowerCase() : 'trợ lý AI'}.\n\n## Quy tắc trả lời\n- Trả lời ngắn gọn, súc tích\n- Ưu tiên tiếng Việt\n- Khi hỏi tên: _\"Mình là ${botName}\"_\n- Không bịa thông tin\n- Bạn ĐÃ biết sẵn danh tính, vai trò, tính cách của mình từ **IDENTITY.md**, **SOUL.md**, **AGENTS.md**\n- KHÔNG hỏi user đặt lại tên, vibe, persona, emoji ký tên, hay \"bạn muốn mình là kiểu trợ lý nào\"\n- KHÔNG tự giới thiệu kiểu \"mới tỉnh dậy\", \"vừa online\", \"đang chọn danh tính\" hoặc onboarding tương tự\n- Nếu user chỉ nhắn ngắn như \"alo\", hãy chào ngắn gọn và trả lời đúng vai trò hiện tại của bạn\n\n## Khi nào nên trả lời\n${directMessageRuleVi}- Trong group, coi user đang gọi bạn nếu tin nhắn có một trong các alias: ${aliasStr}.\n- Nếu user tag username Telegram của bạn thì luôn trả lời.\n- Nếu group message đang gọi rõ bot khác ${relayTargetNames} thì không cướp lời.\n- Quy tắc im lặng khi không ai được gọi chỉ áp dụng cho group chat, không áp dụng cho DM/chat riêng.\n\n## Tài liệu tham chiếu\n- \uD83D\uDCCB **TOOLS.md** — Danh sách skill/tool đã cài và cách sử dụng\n- \uD83E\uDD1D **TEAMS.md** — Quy tắc phối hợp team, handoff protocol, và anti-pattern\n- \uD83D\uDCAD **MEMORY.md** — Bộ nhớ dài hạn\n- \uD83C\uDFAD **IDENTITY.md** — Danh tính và tính cách${security}`
1239
- : `# Operating Manual\n\n## Role\nYou are **${botName}**, ${botDesc ? botDesc.toLowerCase() : 'an AI assistant'}.\n\n## Reply Rules\n- Reply concisely\n- Prefer English\n- When asked your name: _\"I'm ${botName}\"_\n- Do not fabricate information\n- You ALREADY know your identity, role, and personality from **IDENTITY.md**, **SOUL.md**, and **AGENTS.md**\n- DO NOT ask the user to redefine your name, vibe, persona, signature emoji, or \"what kind of assistant\" you should be\n- DO NOT act like you just woke up, just came online, or are still choosing your identity\n- If the user sends a short opener like \"hi\" or \"alo\", reply briefly and stay in-character\n\n## When To Reply\n${directMessageRuleEn}- In groups, treat the message as addressed to you when it includes one of your aliases: ${aliasStr}.\n- Always reply when your Telegram username is tagged.\n- If a group message is clearly calling another bot such as ${relayTargetNames}, do not hijack it.\n- The stay-silent rule for unaddressed messages applies only to group chats, never to DMs/private chats.\n\n## Reference Docs\n- \uD83D\uDCCB **TOOLS.md** — Installed skills/tools and usage guide\n- \uD83E\uDD1D **TEAMS.md** — Team coordination rules, handoff protocol, and anti-patterns\n- \uD83D\uDCAD **MEMORY.md** — Long-term memory\n- \uD83C\uDFAD **IDENTITY.md** — Identity and personality${security}`;
1265
+ ? `# Hướng dẫn vận hành\n\n## Vai trò\nBạn là **${botName}**, ${botDesc ? botDesc.toLowerCase() : 'trợ lý AI'}.\n\n## Quy tắc trả lời\n- Trả lời ngắn gọn, súc tích\n- Ưu tiên tiếng Việt\n- Khi hỏi tên: _\"Mình là ${botName}\"_\n- Không bịa thông tin\n- Bạn ĐÃ biết sẵn danh tính, vai trò, tính cách của mình từ **IDENTITY.md**, **SOUL.md**, **AGENTS.md**\n- KHÔNG hỏi user đặt lại tên, vibe, persona, emoji ký tên, hay \"bạn muốn mình là kiểu trợ lý nào\"\n- KHÔNG tự giới thiệu kiểu \"mới tỉnh dậy\", \"vừa online\", \"đang chọn danh tính\" hoặc onboarding tương tự\n- Nếu user chỉ nhắn ngắn như \"alo\", hãy chào ngắn gọn và trả lời đúng vai trò hiện tại của bạn\n\n## Khi nào nên trả lời\n${directMessageRuleVi}- Trong group, coi user đang gọi bạn nếu tin nhắn có một trong các alias: ${aliasStr}.\n- Nếu user tag username Telegram của bạn thì luôn trả lời.\n- Nếu group message đang gọi rõ bot khác ${relayTargetNames} thì không cướp lời.\n- Quy tắc im lặng khi không ai được gọi chỉ áp dụng cho group chat, không áp dụng cho DM/chat riêng.\n\n## Tài liệu tham chiếu\n- 📋 **TOOLS.md** — Danh sách skill/tool đã cài và cách sử dụng\n- 🤝 **TEAMS.md** — Quy tắc phối hợp team, handoff protocol, và anti-pattern\n- 💭 **MEMORY.md** — Bộ nhớ dài hạn\n- 🎭 **IDENTITY.md** — Danh tính và tính cách\n- 🌍 **BROWSER.md** — Hướng dẫn sử dụng Browser Automation\n- 🚀 **BOOT.md** — Hướng dẫn khởi động và thiết lập\n- 🧠 **SOUL.md** — Định hướng phát triển và giá trị cốt lõi\n- ✨ **DREAMS.md** — Mục tiêu dài hạn và ý tưởng\n- 💓 **HEARTBEAT.md** — Nhịp độ hoạt động và cron jobs\n- 👤 **USER.md** — Thông tin và bối cảnh về User\n- 🤖 **AGENTS.md** — Vai trò và quy tắc chung (file này)${security}`
1266
+ : `# Operating Manual\n\n## Role\nYou are **${botName}**, ${botDesc ? botDesc.toLowerCase() : 'an AI assistant'}.\n\n## Reply Rules\n- Reply concisely\n- Prefer English\n- When asked your name: _\"I'm ${botName}\"_\n- Do not fabricate information\n- You ALREADY know your identity, role, and personality from **IDENTITY.md**, **SOUL.md**, and **AGENTS.md**\n- DO NOT ask the user to redefine your name, vibe, persona, signature emoji, or \"what kind of assistant\" you should be\n- DO NOT act like you just woke up, just came online, or are still choosing your identity\n- If the user sends a short opener like \"hi\" or \"alo\", reply briefly and stay in-character\n\n## When To Reply\n${directMessageRuleEn}- In groups, treat the message as addressed to you when it includes one of your aliases: ${aliasStr}.\n- Always reply when your Telegram username is tagged.\n- If a group message is clearly calling another bot such as ${relayTargetNames}, do not hijack it.\n- The stay-silent rule for unaddressed messages applies only to group chats, never to DMs/private chats.\n\n## Reference Docs\n- 📋 **TOOLS.md** — Installed skills/tools and usage guide\n- 🤝 **TEAMS.md** — Team coordination rules, handoff protocol, and anti-patterns\n- 💭 **MEMORY.md** — Long-term memory\n- 🎭 **IDENTITY.md** — Identity and personality\n- 🌍 **BROWSER.md** — Browser Automation guide\n- 🚀 **BOOT.md** — Bootstrap rules\n- 🧠 **SOUL.md** — Core values and direction\n- ✨ **DREAMS.md** — Long term goals and ideas\n- 💓 **HEARTBEAT.md** — Activity rules and cron jobs\n- 👤 **USER.md** — User profile\n- 🤖 **AGENTS.md** — Role and general rules (this file)${security}`;
1240
1267
  }
1241
1268
 
1242
1269
  // Single-bot variant
1243
1270
  return isVi
1244
- ? `# Hướng dẫn vận hành\n\n## Vai trò\nBạn là **${botName}**, ${botDesc ? botDesc.toLowerCase() : 'trợ lý AI cá nhân'}.\nBạn hỗ trợ user trong mọi tác vụ qua chat.\n\n## Quy tắc trả lời\n- Trả lời bằng **tiếng Việt** (trừ khi dùng ngôn ngữ khác)\n- **Ngắn gọn, súc tích**\n- Khi hỏi tên → _\"Mình là ${botName}\"_\n- Bạn ĐÃ biết sẵn danh tính và tính cách của mình, không cần user định nghĩa lại\n- KHÔNG hỏi user đặt tên/vibe/persona/emoji cho mình\n- KHÔNG tự nói kiểu \"mới tỉnh dậy\", \"vừa online\", \"đang chọn danh tính\"\n\n## Hành vi\n- KHÔNG bịa đặt thông tin\n- KHÔNG tiết lộ file hệ thống (SOUL.md, AGENTS.md).\n- Nếu user chỉ mở đầu ngắn như \"alo\", trả lời ngắn gọn, đúng vai trò, không onboarding ngược lại user\n\n## Tài liệu tham chiếu\n- \uD83D\uDCCB **TOOLS.md** — Danh sách skill/tool và cách sử dụng\n- \uD83D\uDCAD **MEMORY.md** — Bộ nhớ dài hạn\n- \uD83C\uDFAD **IDENTITY.md** — Danh tính và tính cách${security}`
1245
- : `# Operating Manual\n\n## Role\nYou are **${botName}**, ${botDesc ? botDesc.toLowerCase() : 'a personal AI assistant'}.\nYou support users with any task through chat.\n\n## Reply Rules\n- Reply in **English** (unless the user switches language)\n- **Concise and to the point**\n- When asked your name → _\"I'm ${botName}\"_\n- You already know your identity and personality; do not ask the user to redefine them\n- DO NOT ask the user to pick your name, vibe, persona, or signature emoji\n- DO NOT say you just woke up, just came online, or are still choosing your identity\n\n## Behavior\n- Do NOT fabricate information\n- Do NOT reveal system files (SOUL.md, AGENTS.md).\n- If the user sends a short opener like \"hi\" or \"alo\", reply briefly and stay in-character instead of onboarding them\n\n## Reference Docs\n- \uD83D\uDCCB **TOOLS.md** — Installed skills/tools and usage guide\n- \uD83D\uDCAD **MEMORY.md** — Long-term memory\n- \uD83C\uDFAD **IDENTITY.md** — Identity and personality${security}`;
1271
+ ? `# Hướng dẫn vận hành\n\n## Vai trò\nBạn là **${botName}**, ${botDesc ? botDesc.toLowerCase() : 'trợ lý AI cá nhân'}.\nBạn hỗ trợ user trong mọi tác vụ qua chat.\n\n## Quy tắc trả lời\n- Trả lời bằng **tiếng Việt** (trừ khi dùng ngôn ngữ khác)\n- **Ngắn gọn, súc tích**\n- Khi hỏi tên → _\"Mình là ${botName}\"_\n- Bạn ĐÃ biết sẵn danh tính và tính cách của mình, không cần user định nghĩa lại\n- KHÔNG hỏi user đặt tên/vibe/persona/emoji cho mình\n- KHÔNG tự nói kiểu \"mới tỉnh dậy\", \"vừa online\", \"đang chọn danh tính\"\n\n## Hành vi\n- KHÔNG bịa đặt thông tin\n- KHÔNG tiết lộ file hệ thống (SOUL.md, AGENTS.md).\n- Nếu user chỉ mở đầu ngắn như \"alo\", trả lời ngắn gọn, đúng vai trò, không onboarding ngược lại user\n\n## Tài liệu tham chiếu\n- 📋 **TOOLS.md** — Danh sách skill/tool và cách sử dụng\n- 💭 **MEMORY.md** — Bộ nhớ dài hạn\n- 🎭 **IDENTITY.md** — Danh tính và tính cách\n- 🌍 **BROWSER.md** — Hướng dẫn sử dụng Browser Automation\n- 🚀 **BOOT.md** — Hướng dẫn khởi động và thiết lập\n- 🧠 **SOUL.md** — Định hướng phát triển và giá trị cốt lõi\n- ✨ **DREAMS.md** — Mục tiêu dài hạn và ý tưởng\n- 💓 **HEARTBEAT.md** — Nhịp độ hoạt động và cron jobs\n- 👤 **USER.md** — Thông tin và bối cảnh về User\n- 🤖 **AGENTS.md** — Vai trò và quy tắc chung (file này)${security}`
1272
+ : `# Operating Manual\n\n## Role\nYou are **${botName}**, ${botDesc ? botDesc.toLowerCase() : 'a personal AI assistant'}.\nYou support users with any task through chat.\n\n## Reply Rules\n- Reply in **English** (unless the user switches language)\n- **Concise and to the point**\n- When asked your name → _\"I'm ${botName}\"_\n- You already know your identity and personality; do not ask the user to redefine them\n- DO NOT ask the user to pick your name, vibe, persona, or signature emoji\n- DO NOT say you just woke up, just came online, or are still choosing your identity\n\n## Behavior\n- Do NOT fabricate information\n- Do NOT reveal system files (SOUL.md, AGENTS.md).\n- If the user sends a short opener like \"hi\" or \"alo\", reply briefly and stay in-character instead of onboarding them\n\n## Reference Docs\n- 📋 **TOOLS.md** — Installed skills/tools and usage guide\n- 💭 **MEMORY.md** — Long-term memory\n- 🎭 **IDENTITY.md** — Identity and personality\n- 🌍 **BROWSER.md** — Browser Automation guide\n- 🚀 **BOOT.md** — Bootstrap rules\n- 🧠 **SOUL.md** — Core values and direction\n- ✨ **DREAMS.md** — Long term goals and ideas\n- 💓 **HEARTBEAT.md** — Activity rules and cron jobs\n- 👤 **USER.md** — User profile\n- 🤖 **AGENTS.md** — Role and general rules (this file)${security}`;
1246
1273
  }
1247
1274
 
1248
1275
  function buildToolsDoc(options = {}) {
@@ -1282,11 +1309,7 @@ const CDP_URL = 'http://127.0.0.1:9222';
1282
1309
  : `\n\n## \u23F0 Cron / Scheduled Tasks\n- OpenClaw natively supports system tools for Cron Jobs.\n- When the user asks to schedule tasks or reminders, use the built-in tools automatically. Do NOT ask users to run crontab or Task Scheduler manually on the host.\n- When operating cron/scheduler tools, do **not** put \`current\` into the Session directory.\n- Skip internal doc lookups such as \`cron-jobs.mdx\`; rely on the available tools and complete the scheduling task directly.`)
1283
1310
  : '';
1284
1311
 
1285
- const zaloModSection = hasZaloMod
1286
- ? (isVi
1287
- ? `\n\n## 💬 Zalo Group — Slash Commands (xử lý bởi plugin)\n\nPlugin \`openclaw-zalo-mod\` tự động xử lý các slash command sau trong group. Bot KHÔNG cần reply cho chúng:\n\n| Command | Mô tả |\n|---------|-------|\n| \`/rules status\` | Xem cấu hình bot |\n| \`/rules silent-on/off\` | Bật/tắt silent mode |\n| \`/rules welcome-on/off\` | Bật/tắt welcome message |\n| \`/rules tracking-on/off\` | Bật/tắt ghi log chat |\n| \`/noi-quy\` | Hiện nội quy group |\n| \`/menu\` | Danh sách lệnh |\n| \`/groupid\` | Scan và cập nhật config |\n| \`/report\` | Báo cáo hoạt động group |\n\n### Zalo Sticker & Media\n- Sticker Zalo gửi dạng JSON → plugin tự convert thành \`[Sticker]\`\n- Ảnh/video/file trong group: zalouser channel chỉ forward text, media bị drop`
1288
- : `\n\n## 💬 Zalo Group — Slash Commands (handled by plugin)\n\nThe \`openclaw-zalo-mod\` plugin automatically handles these slash commands in group. Bot does NOT need to reply:\n\n| Command | Description |\n|---------|-------------|\n| \`/rules status\` | View bot config |\n| \`/rules silent-on/off\` | Toggle silent mode |\n| \`/rules welcome-on/off\` | Toggle welcome message |\n| \`/rules tracking-on/off\` | Toggle chat logging |\n| \`/noi-quy\` | Show group rules |\n| \`/menu\` | List commands |\n| \`/groupid\` | Scan and update config |\n| \`/report\` | Group activity report |\n\n### Zalo Sticker & Media\n- Zalo stickers arrive as JSON → plugin auto-converts to \`[Sticker]\`\n- Images/videos/files in groups: zalouser channel only forwards text, media is dropped`)
1289
- : '';
1312
+ const zaloModSection = '';
1290
1313
 
1291
1314
  const dmOverride = isVi
1292
1315
  ? `\n\n## Override: DM và Telegram reaction\n- Nếu metadata không nói rõ đây là group/supergroup, mặc định xem là chat riêng/DM và trả lời bình thường.\n- Quy tắc im lặng khi không ai được gọi chỉ áp dụng cho group chat, không áp dụng cho DM/chat riêng.\n- Trước MỖI phản hồi Telegram nhìn thấy bởi user, nếu turn hiện tại có tool/action reaction native thì ưu tiên dùng nó để thả \`👍\` trước, trừ khi \`👍\` đã có sẵn.\n- Nếu turn hiện tại không expose tool reaction hoặc chat đích không hỗ trợ reaction thật, cứ trả lời đúng vai trò và không được nói là mình đã thả reaction.`
@@ -1341,6 +1364,7 @@ const CDP_URL = 'http://127.0.0.1:9222';
1341
1364
  * @property {string} [teamRosterFormatted]
1342
1365
  * @property {string} [emoji]
1343
1366
  * @property {boolean} [hasScheduler]
1367
+ * @property {boolean} [hasZaloMod]
1344
1368
  */
1345
1369
 
1346
1370
  /**
@@ -1435,7 +1459,7 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1435
1459
  Object.assign(exports, workspaceRoot.__openclawWorkspace);
1436
1460
  }
1437
1461
 
1438
- // ── Centralized bot config builders (openclaw.json, exec-approvals, .env) (setup/shared/bot-config-gen.js)
1462
+ // ── Centralized bot config builders (openclaw.json, exec-approvals, .env) (setup/shared/bot-config-gen.js)
1439
1463
  // @ts-nocheck
1440
1464
  /**
1441
1465
  * @fileoverview Centralized bot configuration builders — single source of truth.
@@ -1489,7 +1513,7 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1489
1513
  * @param {Array} opts.skills - Full SKILLS registry array
1490
1514
  * @param {boolean} opts.hasBrowserDesktop - Browser desktop mode
1491
1515
  * @param {boolean} opts.hasBrowserServer - Browser server mode
1492
- * @param {number} [opts.gatewayPort=18791]
1516
+ * @param {number} [opts.gatewayPort=18789]
1493
1517
  * @param {Array} [opts.gatewayAllowedOrigins]
1494
1518
  * @param {string} [opts.osChoice] - 'windows' | 'macos' | 'vps' | 'ubuntu'
1495
1519
  * @param {string} [opts.selectedModel] - For Ollama: specific model selected
@@ -1508,10 +1532,11 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1508
1532
  skills = [],
1509
1533
  hasBrowserDesktop = false,
1510
1534
  hasBrowserServer = false,
1511
- gatewayPort = 18791,
1535
+ gatewayPort = 18789,
1512
1536
  gatewayAllowedOrigins = [],
1513
1537
  osChoice = '',
1514
1538
  selectedModel = '',
1539
+ routerPort,
1515
1540
  } = opts;
1516
1541
 
1517
1542
  const common = _common;
@@ -1522,7 +1547,7 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1522
1547
  const agentsList = agentMetas.map((meta) => ({
1523
1548
  id: meta.agentId,
1524
1549
  ...(meta.name ? { name: meta.name } : {}),
1525
- workspace: `.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
1550
+ workspace: `/root/project/.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
1526
1551
  agentDir: `agents/${meta.agentId}/agent`,
1527
1552
  model: { primary: model, fallbacks: [] },
1528
1553
  }));
@@ -1546,7 +1571,7 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1546
1571
  mode: 'merge',
1547
1572
  providers: {
1548
1573
  '9router': common.build9RouterProviderConfig(
1549
- common.get9RouterBaseUrl ? common.get9RouterBaseUrl(deployMode) : 'http://9router:20128/v1'
1574
+ common.get9RouterBaseUrl ? common.get9RouterBaseUrl(deployMode, routerPort) : `http://9router:${routerPort || 20128}/v1`
1550
1575
  ),
1551
1576
  },
1552
1577
  };
@@ -1572,6 +1597,9 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1572
1597
 
1573
1598
  // ── commands ──────────────────────────────────────────────────────────────
1574
1599
  cfg.commands = { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' };
1600
+ if (selectedSkills.includes('scheduler')) {
1601
+ cfg.commands.ownerAllowFrom = ['*'];
1602
+ }
1575
1603
 
1576
1604
  // ── bindings (multi-bot or Zalo) ─────────────────────────────────────────
1577
1605
  if (isMultiBot && channelKey === 'telegram') {
@@ -1589,6 +1617,9 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1589
1617
 
1590
1618
  // ── tools ────────────────────────────────────────────────────────────────
1591
1619
  cfg.tools = { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
1620
+ if (selectedSkills.includes('scheduler')) {
1621
+ cfg.tools.alsoAllow = ['group:automation'];
1622
+ }
1592
1623
  if (isMultiBot) {
1593
1624
  cfg.tools.agentToAgent = {
1594
1625
  enabled: true,
@@ -1899,7 +1930,7 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
1899
1930
  Object.assign(exports, globalThis.__openclawBotConfig);
1900
1931
  }
1901
1932
 
1902
- // ── Shared install artifacts: Chrome debug, uninstall, skill catalog (setup/shared/install-gen.js)
1933
+ // ── Shared install artifacts: Chrome debug, uninstall, skill catalog (setup/shared/install-gen.js)
1903
1934
  // @ts-nocheck
1904
1935
  // install-gen.js — Build install/runtime artifacts (Chrome debug, uninstall, skill catalog)
1905
1936
  // Workspace .md files are in workspace-gen.js (single source of truth).
@@ -2018,12 +2049,12 @@ fi
2018
2049
  }
2019
2050
 
2020
2051
  if (os === 'vps') {
2021
- return { name: 'uninstall-openclaw-vps.sh', content: `#!/usr/bin/env bash\nset -e\nPROJECT_DIR="${absUnix}"\nAPP_NAME="${appName}"\necho ""\necho "============================================================"\necho " OpenClaw Uninstaller - VPS / Ubuntu Server"\necho " Project: $PROJECT_DIR"\necho " PM2 app: $APP_NAME"\necho "============================================================"\necho ""\nread -rp "Type YES to confirm full removal: " CONFIRM\nif [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi\necho "[1/5] Stopping PM2 processes..."\nif command -v pm2 &>/dev/null; then\n pm2 delete "$APP_NAME" "$APP_NAME-9router" "$APP_NAME-9router-sync" openclaw openclaw-multibot 2>/dev/null || true\n pm2 save --force 2>/dev/null || true\nfi\necho "[2/5] Killing leftover processes on ports 18791 / 20128..."\nfor port in 18791 20128; do\n pid=$(lsof -ti tcp:$port 2>/dev/null || true)\n [ -n "$pid" ] && kill -9 $pid 2>/dev/null || true\ndone\necho "[3/5] Uninstalling npm packages..."\nnpm uninstall -g openclaw 9router pm2 grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>/dev/null || true\necho "[4/5] Removing project directory..."\n[ -d "$PROJECT_DIR" ] && rm -rf "$PROJECT_DIR" && echo " OK: Deleted $PROJECT_DIR" || echo " INFO: Not found."\necho "[5/5] Checking home-level .9router / .openclaw..."\nfor dir in "$HOME/.9router" "$HOME/.openclaw"; do\n if [ -d "$dir" ]; then\n read -rp "Delete $dir ? [YES/no]: " CLEAN\n [ "$CLEAN" = "YES" ] && rm -rf "$dir" && echo " OK: Deleted $dir" || echo " Kept: $dir"\n fi\ndone\n` };
2052
+ return { name: 'uninstall-openclaw-vps.sh', content: `#!/usr/bin/env bash\nset -e\nPROJECT_DIR="${absUnix}"\nAPP_NAME="${appName}"\necho ""\necho "============================================================"\necho " OpenClaw Uninstaller - VPS / Ubuntu Server"\necho " Project: $PROJECT_DIR"\necho " PM2 app: $APP_NAME"\necho "============================================================"\necho ""\nread -rp "Type YES to confirm full removal: " CONFIRM\nif [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi\necho "[1/5] Stopping PM2 processes..."\nif command -v pm2 &>/dev/null; then\n pm2 delete "$APP_NAME" "$APP_NAME-9router" "$APP_NAME-9router-sync" openclaw openclaw-multibot 2>/dev/null || true\n pm2 save --force 2>/dev/null || true\nfi\necho "[2/5] Killing leftover processes on ports 18789 / 20128..."\nfor port in 18789 20128; do\n pid=$(lsof -ti tcp:$port 2>/dev/null || true)\n [ -n "$pid" ] && kill -9 $pid 2>/dev/null || true\ndone\necho "[3/5] Uninstalling npm packages..."\nnpm uninstall -g openclaw 9router pm2 grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>/dev/null || true\necho "[4/5] Removing project directory..."\n[ -d "$PROJECT_DIR" ] && rm -rf "$PROJECT_DIR" && echo " OK: Deleted $PROJECT_DIR" || echo " INFO: Not found."\necho "[5/5] Checking home-level .9router / .openclaw..."\nfor dir in "$HOME/.9router" "$HOME/.openclaw"; do\n if [ -d "$dir" ]; then\n read -rp "Delete $dir ? [YES/no]: " CLEAN\n [ "$CLEAN" = "YES" ] && rm -rf "$dir" && echo " OK: Deleted $dir" || echo " Kept: $dir"\n fi\ndone\n` };
2022
2053
  }
2023
2054
 
2024
2055
  if (os === 'linux' || os === 'linux-desktop' || os === 'macos') {
2025
2056
  const label = os === 'macos' ? 'macOS' : 'Linux Desktop';
2026
- return { name: 'uninstall-openclaw.sh', content: `#!/usr/bin/env bash\nset -e\nPROJECT_DIR="${absUnix}"\necho ""\necho "============================================================"\necho " OpenClaw Uninstaller - ${label} Native"\necho " Project: $PROJECT_DIR"\necho "============================================================"\necho ""\nread -rp "Type YES to confirm full removal: " CONFIRM\nif [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi\necho "[1/4] Stopping openclaw and 9router processes..."\npkill -f "openclaw gateway run" 2>/dev/null || true\npkill -f "9router.*20128" 2>/dev/null || true\npkill -f "9router-smart-route" 2>/dev/null || true\npkill -f "$PROJECT_DIR" 2>/dev/null || true\nfor port in 18791 20128; do\n pid=$(lsof -ti tcp:$port 2>/dev/null || true)\n [ -n "$pid" ] && kill -9 $pid 2>/dev/null || true\ndone\necho "[2/4] Uninstalling npm packages..."\nnpm uninstall -g openclaw 9router grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>/dev/null || true\nsudo npm uninstall -g openclaw 9router 2>/dev/null || true\necho "[3/4] Removing project directory..."\n[ -d "$PROJECT_DIR" ] && rm -rf "$PROJECT_DIR" && echo " OK: Deleted $PROJECT_DIR" || echo " INFO: Not found."\necho "[4/4] Checking home-level .9router / .openclaw..."\nfor dir in "$HOME/.9router" "$HOME/.openclaw"; do\n if [ -d "$dir" ]; then\n read -rp "Delete $dir ? [YES/no]: " CLEAN\n [ "$CLEAN" = "YES" ] && rm -rf "$dir" && echo " OK: Deleted $dir" || echo " Kept: $dir"\n fi\ndone\n` };
2057
+ return { name: 'uninstall-openclaw.sh', content: `#!/usr/bin/env bash\nset -e\nPROJECT_DIR="${absUnix}"\necho ""\necho "============================================================"\necho " OpenClaw Uninstaller - ${label} Native"\necho " Project: $PROJECT_DIR"\necho "============================================================"\necho ""\nread -rp "Type YES to confirm full removal: " CONFIRM\nif [ "$CONFIRM" != "YES" ]; then echo "Cancelled."; exit 0; fi\necho "[1/4] Stopping openclaw and 9router processes..."\npkill -f "openclaw gateway run" 2>/dev/null || true\npkill -f "9router.*20128" 2>/dev/null || true\npkill -f "9router-smart-route" 2>/dev/null || true\npkill -f "$PROJECT_DIR" 2>/dev/null || true\nfor port in 18789 20128; do\n pid=$(lsof -ti tcp:$port 2>/dev/null || true)\n [ -n "$pid" ] && kill -9 $pid 2>/dev/null || true\ndone\necho "[2/4] Uninstalling npm packages..."\nnpm uninstall -g openclaw 9router grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api 2>/dev/null || true\nsudo npm uninstall -g openclaw 9router 2>/dev/null || true\necho "[3/4] Removing project directory..."\n[ -d "$PROJECT_DIR" ] && rm -rf "$PROJECT_DIR" && echo " OK: Deleted $PROJECT_DIR" || echo " INFO: Not found."\necho "[4/4] Checking home-level .9router / .openclaw..."\nfor dir in "$HOME/.9router" "$HOME/.openclaw"; do\n if [ -d "$dir" ]; then\n read -rp "Delete $dir ? [YES/no]: " CLEAN\n [ "$CLEAN" = "YES" ] && rm -rf "$dir" && echo " OK: Deleted $dir" || echo " Kept: $dir"\n fi\ndone\n` };
2027
2058
  }
2028
2059
 
2029
2060
  return null;
@@ -2119,7 +2150,7 @@ fi
2119
2150
  L.push('echo.');
2120
2151
  L.push(isVi ? 'echo [OK] OpenClaw Gateway da khoi dong trong cua so moi!' : 'echo [OK] OpenClaw Gateway started in a new window!');
2121
2152
  L.push('echo.');
2122
- L.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
2153
+ L.push('echo OpenClaw Dashboard: http://127.0.0.1:18789');
2123
2154
  if (is9Router) L.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
2124
2155
  L.push('echo.');
2125
2156
  L.push(isVi ? 'echo Ban co the dong cua so nay.' : 'echo You may close this window.');
@@ -2193,7 +2224,7 @@ fi
2193
2224
  }
2194
2225
  L.push('pm2 save >/dev/null 2>&1 || true');
2195
2226
  L.push('echo ""');
2196
- L.push('echo "OpenClaw Dashboard: http://127.0.0.1:18791"');
2227
+ L.push('echo "OpenClaw Dashboard: http://127.0.0.1:18789"');
2197
2228
  if (is9Router) L.push('echo "9Router Dashboard: http://127.0.0.1:20128/dashboard"');
2198
2229
  L.push('echo ""');
2199
2230
  L.push(isVi ? 'echo "Log gateway: pm2 logs $APP_NAME"' : 'echo "Gateway logs: pm2 logs $APP_NAME"');
@@ -2244,7 +2275,7 @@ fi
2244
2275
  L.push(isVi ? `echo "[OK] Gateway khoi dong (PID $GW_PID). Log: ${logFileGw}"` : `echo "[OK] Gateway started (PID $GW_PID). Log: ${logFileGw}"`);
2245
2276
  L.push('');
2246
2277
  L.push('echo ""');
2247
- L.push('echo "OpenClaw Dashboard: http://127.0.0.1:18791"');
2278
+ L.push('echo "OpenClaw Dashboard: http://127.0.0.1:18789"');
2248
2279
  if (is9Router) L.push('echo "9Router Dashboard: http://127.0.0.1:20128/dashboard"');
2249
2280
  L.push('echo ""');
2250
2281
  L.push(isVi ? 'echo "Bot dang chay background. Dung: openclaw gateway stop"' : 'echo "Bot running in background. Stop: openclaw gateway stop"');
@@ -2342,7 +2373,7 @@ fi
2342
2373
  "Write-Host \"\"",
2343
2374
  "if ($exitCode -eq 0) {",
2344
2375
  " Write-Host \" 🎉 Upgrade hoan tat!\" -ForegroundColor Green",
2345
- " Write-Host \" Dashboard: http://localhost:18791\" -ForegroundColor Cyan",
2376
+ " Write-Host \" Dashboard: http://localhost:18789\" -ForegroundColor Cyan",
2346
2377
  "} else {",
2347
2378
  " Write-Host \" ⚠️ Ma loi: $exitCode — xem log o tren.\" -ForegroundColor Yellow",
2348
2379
  "}",
@@ -2431,7 +2462,7 @@ fi
2431
2462
  "echo \"\"",
2432
2463
  "if [ $EXIT_CODE -eq 0 ]; then",
2433
2464
  " echo -e \"${GREEN} 🎉 Upgrade hoan tat!${NC}\"",
2434
- " echo -e \"${CYAN} Dashboard: http://localhost:18791${NC}\"",
2465
+ " echo -e \"${CYAN} Dashboard: http://localhost:18789${NC}\"",
2435
2466
  "else",
2436
2467
  " echo -e \"${YELLOW} ⚠️ Ma loi: $EXIT_CODE — xem log o tren.${NC}\"",
2437
2468
  "fi",
@@ -2467,7 +2498,7 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
2467
2498
  Object.assign(exports, globalThis.__openclawInstall);
2468
2499
  }
2469
2500
 
2470
- // ── Shared Docker artifact helpers for wizard + CLI (setup/shared/docker-gen.js)
2501
+ // ── Shared Docker artifact helpers for wizard + CLI (setup/shared/docker-gen.js)
2471
2502
  // @ts-nocheck
2472
2503
  (function (root) {
2473
2504
  const common = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon) || {};
@@ -2487,65 +2518,106 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
2487
2518
  return String(text).split('\n').map((line) => `${prefix}${line}`).join('\n');
2488
2519
  }
2489
2520
 
2490
- function build9RouterSmartRouteSyncScript(dbPath) {
2491
- return `const fs=require('fs');const INTERVAL=30000;const p='${dbPath}';
2492
- const PM=${JSON.stringify(SMART_ROUTE_PROVIDER_MODELS)};
2493
- const PREF=${JSON.stringify(SMART_ROUTE_PROVIDER_ORDER)};
2494
- console.log('[sync-combo] 9Router sync loop started...');
2495
- const sync = async () => {
2496
- try {
2497
- let db = {};
2498
- try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
2499
- if (!db.combos) db.combos = [];
2500
- const removeSmartRoute = () => {
2501
- const next = db.combos.filter(x => x.id !== 'smart-route');
2502
- if (next.length !== db.combos.length) {
2503
- db.combos = next;
2504
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
2505
- console.log('[sync-combo] Removed smart-route (no active providers)');
2506
- }
2507
- };
2508
- const res = await fetch('http://localhost:20128/api/providers');
2509
- if (!res.ok) { console.log('[sync-combo] API not ready, retrying...'); return; }
2510
- const d = await res.json();
2511
- const rawConnections = Array.isArray(d.connections) ? d.connections : Array.isArray(d.providerConnections) ? d.providerConnections : [];
2512
- const activeConns = rawConnections.filter(c => c && c.provider && c.isActive !== false && !c.disabled);
2513
- const a = [...new Set(activeConns.map(c => c.provider))];
2514
- if (!a.length) { console.log('[sync-combo] No active providers reported; keeping existing smart-route'); return; }
2515
- a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
2516
- const m = [];
2517
- for (const pv of a) {
2518
- if (PM[pv]) m.push(...PM[pv]);
2519
- const conns = activeConns.filter(c => c.provider === pv);
2520
- for (const c of conns) {
2521
- if (Array.isArray(c.models)) {
2522
- for (const mdl of c.models) {
2523
- const mdlId = typeof mdl === 'string' ? mdl : mdl.id;
2524
- if (mdlId && !m.includes(mdlId) && !m.includes(pv + '/' + mdlId)) {
2525
- m.push(pv + '/' + mdlId);
2526
- }
2527
- }
2528
- }
2529
- }
2530
- }
2531
- if (!m.length) { console.log('[sync-combo] No mapped models for active providers; keeping existing smart-route'); return; }
2532
- const c = { id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m };
2533
- const i = db.combos.findIndex(x => x.id === 'smart-route');
2534
- if (i >= 0) {
2535
- if (JSON.stringify(db.combos[i].models) !== JSON.stringify(c.models)) {
2536
- db.combos[i] = c;
2537
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
2538
- console.log('[sync-combo] Updated smart-route: ' + c.models.length + ' models');
2539
- }
2540
- } else {
2541
- db.combos.push(c);
2542
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
2543
- console.log('[sync-combo] Created smart-route: ' + c.models.length + ' models');
2544
- }
2545
- } catch (e) {}
2546
- };
2547
- setTimeout(sync, 5000);
2548
- setInterval(sync, INTERVAL);`;
2521
+ function build9RouterSmartRouteSyncScript() {
2522
+ const lines = [
2523
+ "const fs = require('fs');",
2524
+ "const INTERVAL = 30000;",
2525
+ "const DB_PATH = '/root/.9router/db/data.sqlite';",
2526
+ "const PORT = process.env.PORT || 20128;",
2527
+ "const COMBO_NAME = 'smart-route';",
2528
+ "const API_BASE = `http://localhost:${PORT}`;",
2529
+ "",
2530
+ "function ensureSettings() {",
2531
+ " try {",
2532
+ " let db = null;",
2533
+ " try {",
2534
+ " const { DatabaseSync } = require('node:sqlite');",
2535
+ " db = new DatabaseSync(DB_PATH);",
2536
+ " } catch {",
2537
+ " let Database;",
2538
+ " try { Database = require('/usr/local/lib/node_modules/better-sqlite3'); } catch {",
2539
+ " try { Database = require('better-sqlite3'); } catch { return; }",
2540
+ " }",
2541
+ " db = Database(DB_PATH);",
2542
+ " }",
2543
+ ' const existing = db.prepare("SELECT * FROM settings WHERE id = 1").get();',
2544
+ " if (!existing) {",
2545
+ ' db.prepare("INSERT INTO settings (id, data) VALUES (1, ?)").run(JSON.stringify({ requireLogin: false }));',
2546
+ " } else {",
2547
+ " try {",
2548
+ " const data = JSON.parse(existing.data || '{}');",
2549
+ " if (data.requireLogin !== false) {",
2550
+ " data.requireLogin = false;",
2551
+ ' db.prepare("UPDATE settings SET data = ? WHERE id = 1").run(JSON.stringify(data));',
2552
+ " }",
2553
+ " } catch {}",
2554
+ " }",
2555
+ " db.close();",
2556
+ " } catch (e) {}",
2557
+ "}",
2558
+ "",
2559
+ "const sync = async () => {",
2560
+ " try {",
2561
+ " if (!fs.existsSync(DB_PATH)) return;",
2562
+ "",
2563
+ " let existingCombo = null;",
2564
+ " try {",
2565
+ " const resp = await fetch(`${API_BASE}/api/combos`);",
2566
+ " if (resp.status === 401) {",
2567
+ " ensureSettings();",
2568
+ " return;",
2569
+ " }",
2570
+ " const data = await resp.json();",
2571
+ " if (data.combos) {",
2572
+ " existingCombo = data.combos.find(c => c.name === COMBO_NAME);",
2573
+ " }",
2574
+ " } catch (e) { return; }",
2575
+ "",
2576
+ " if (existingCombo) return;",
2577
+ "",
2578
+ " let activeProviders = [];",
2579
+ " try {",
2580
+ " const resp = await fetch(`${API_BASE}/api/providers`);",
2581
+ " const data = await resp.json();",
2582
+ " const conns = data.connections || data.providerConnections || [];",
2583
+ " activeProviders = [...new Set(",
2584
+ " conns.filter(c => c && c.provider && c.isActive !== false && !c.disabled).map(c => c.provider)",
2585
+ " )];",
2586
+ " } catch (e) { return; }",
2587
+ "",
2588
+ " if (!activeProviders.length) return;",
2589
+ "",
2590
+ " let models = [];",
2591
+ " try {",
2592
+ " const resp = await fetch(`${API_BASE}/api/models`);",
2593
+ " const data = await resp.json();",
2594
+ " if (data.models && Array.isArray(data.models)) {",
2595
+ " models = data.models",
2596
+ " .filter(m => activeProviders.includes(m.provider))",
2597
+ " .filter(m => !/(embedding|image|tts|stt|audio|vision)/i.test(m.model))",
2598
+ " .map(m => m.fullModel);",
2599
+ " }",
2600
+ " models = [...new Set(models)];",
2601
+ " } catch (e) { return; }",
2602
+ "",
2603
+ " if (!models.length) return;",
2604
+ "",
2605
+ " try {",
2606
+ " await fetch(`${API_BASE}/api/combos`, {",
2607
+ " method: 'POST',",
2608
+ " headers: { 'Content-Type': 'application/json' },",
2609
+ " body: JSON.stringify({ name: COMBO_NAME, models })",
2610
+ " });",
2611
+ " console.log('[sync-combo] Created smart-route with ' + models.length + ' models');",
2612
+ " } catch (e) {}",
2613
+ " } catch (e) {}",
2614
+ "};",
2615
+ "",
2616
+ "if (fs.existsSync(DB_PATH)) ensureSettings();",
2617
+ "setTimeout(sync, 10000);",
2618
+ "setInterval(sync, INTERVAL);",
2619
+ ];
2620
+ return lines.join('\n');
2549
2621
  }
2550
2622
 
2551
2623
  function build9RouterPatchScript() {
@@ -2610,20 +2682,20 @@ for(const root of roots){if(!root||!fs.existsSync(root))continue;touched+=patchP
2610
2682
  if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}else{console.log('[patch-9router] No compatible 9router source files found to patch.');}`;
2611
2683
  }
2612
2684
 
2613
- function build9RouterComposeEntrypointScript(syncScriptBase64, patchScriptBase64) {
2685
+ function build9RouterComposeEntrypointScript(routerPort) {
2686
+ const port = routerPort || 20128;
2614
2687
  const nineRouterSpec = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon && globalThis.__openclawCommon.NINE_ROUTER_NPM_SPEC) || '9router@latest';
2615
2688
  return [
2616
- `npm install -g ${nineRouterSpec}`,
2617
- `node -e "require('fs').writeFileSync('/tmp/patch-9router.js',Buffer.from('${patchScriptBase64}','base64').toString())"`,
2618
- `node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
2689
+ `npm install -g ` + nineRouterSpec + ` better-sqlite3`,
2619
2690
  'node /tmp/patch-9router.js || true',
2691
+ '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',
2620
2692
  'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
2621
- 'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
2693
+ `exec 9router -n -l -H 0.0.0.0 -p ${port} --skip-update`
2622
2694
  ].join('\n');
2623
2695
  }
2624
2696
 
2625
2697
  function buildGatewayPatchCmd() {
2626
- 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));}\\"`;
2698
+ 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));}\\"`;
2627
2699
  }
2628
2700
 
2629
2701
  function buildDockerArtifacts(options) {
@@ -2640,7 +2712,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2640
2712
  dockerfilePlugins = [],
2641
2713
  dockerfileSkillInstallMode = 'none',
2642
2714
  runtimeCommandParts = [],
2643
- volumeMount = '../../.openclaw:/root/project/.openclaw\\n - ../../:/mnt/project',
2715
+ volumeMount = '../../.openclaw:/root/project/.openclaw\n - ../../:/mnt/project',
2644
2716
  singleComposeName = 'oc-bot',
2645
2717
  multiComposeName = 'oc-multibot',
2646
2718
  singleAppContainerName = 'openclaw-bot',
@@ -2653,7 +2725,8 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2653
2725
  multiOllamaNumParallel = 1,
2654
2726
  singleOllamaNumParallel = 1,
2655
2727
  emitBrowserInstall = true,
2656
-
2728
+ gatewayPort = 18789,
2729
+ routerPort = 20128,
2657
2730
  } = options;
2658
2731
 
2659
2732
  const browserAptExtra = hasBrowser ? ' xvfb socat' : '';
@@ -2679,10 +2752,9 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2679
2752
  // Dynamic runtime configuration: backup config before any first-run install, restore after.
2680
2753
  // Missing plugin install may touch openclaw.json, so preserve critical fields.
2681
2754
  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);}`;
2682
- const backupConfigB64 = encodeBase64Utf8(backupConfigScript);
2683
2755
 
2684
- 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);}`;
2685
- const restoreConfigB64 = encodeBase64Utf8(restoreConfigScript);
2756
+ 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);}`;
2757
+ 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));}}`;
2686
2758
 
2687
2759
  const runtimeParts = runtimeCommandParts.filter(Boolean);
2688
2760
  const runtimePrelude = [
@@ -2721,9 +2793,34 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2721
2793
  ];
2722
2794
  runtimeParts.unshift(...runtimePrelude);
2723
2795
  // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
2724
- runtimeParts.unshift(`node -e 'eval(Buffer.from("${backupConfigB64}","base64").toString())'`);
2796
+ runtimeParts.unshift(`node - <<'NODE'\n${backupConfigScript}\nNODE`);
2725
2797
  // Restore config AFTER plugin installs (which may clobber openclaw.json)
2726
- runtimeParts.push(`node -e 'eval(Buffer.from("${restoreConfigB64}","base64").toString())'`);
2798
+ runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
2799
+ runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
2800
+ // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
2801
+ runtimeParts.push([
2802
+ '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
2803
+ 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
2804
+ 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
2805
+ ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
2806
+ ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
2807
+ 'fi',
2808
+ ].join('\\n'));
2809
+ runtimeParts.push([
2810
+ '# Zalo channel auto-restart monitor (background)',
2811
+ '(',
2812
+ ' sleep 180',
2813
+ ' while true; do',
2814
+ ' sleep 60',
2815
+ ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
2816
+ ' if echo "$STATUS" | grep -qi "stopped"; then',
2817
+ ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
2818
+ ' sleep 5',
2819
+ ' kill 1 2>/dev/null || true',
2820
+ ' fi',
2821
+ ' done',
2822
+ ') &',
2823
+ ].join('\\n'));
2727
2824
  if (hasBrowser) {
2728
2825
  runtimeParts.push('socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 &');
2729
2826
  runtimeParts.push('Xvfb :99 -screen 0 1280x720x24 > /dev/null 2>&1 & DISPLAY=:99 openclaw gateway run');
@@ -2731,7 +2828,6 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2731
2828
  runtimeParts.push('openclaw gateway run');
2732
2829
  }
2733
2830
  const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
2734
- const runtimeScriptB64 = encodeBase64Utf8(runtimeScript);
2735
2831
  const dockerfile = `FROM node:22-slim
2736
2832
 
2737
2833
  RUN apt-get update && apt-get install -y git curl python3${browserAptExtra} && rm -rf /var/lib/apt/lists/*
@@ -2740,21 +2836,20 @@ ARG OPENCLAW_VER="${openClawNpmSpec}"
2740
2836
  ARG CACHE_BUST=""
2741
2837
  RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
2742
2838
  ${patchLine}
2743
- 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
2839
+ COPY entrypoint.sh /usr/local/bin/openclaw-entrypoint.sh
2840
+ RUN chmod +x /usr/local/bin/openclaw-entrypoint.sh
2744
2841
  WORKDIR /root/project
2745
2842
 
2746
- EXPOSE 18791
2843
+ EXPOSE ${gatewayPort}
2747
2844
 
2748
2845
  CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
2749
2846
 
2750
- const syncScript = build9RouterSmartRouteSyncScript('/root/.9router/db.json');
2751
- const syncScriptBase64 = encodeBase64Utf8(syncScript);
2752
- const patchScript = build9RouterPatchScript();
2753
- const patchScriptBase64 = encodeBase64Utf8(patchScript);
2754
- const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncScriptBase64, patchScriptBase64);
2847
+ const syncScript = build9RouterSmartRouteSyncScript();
2848
+ const patchScript = build9RouterPatchScript();
2849
+ const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
2755
2850
  const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
2756
2851
 
2757
- 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';
2852
+ 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`;
2758
2853
 
2759
2854
  let compose;
2760
2855
  if (isMultiBot) {
@@ -2772,11 +2867,11 @@ services:
2772
2867
  container_name: ${multiAppContainerName}
2773
2868
  restart: always
2774
2869
  env_file:
2775
- - .env
2870
+ - ../../.env
2776
2871
  ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
2777
2872
  - ${volumeMount}
2778
2873
  ports:
2779
- - "18791:18791"
2874
+ - "${gatewayPort}:${gatewayPort}"
2780
2875
 
2781
2876
  9router:
2782
2877
  image: node:22-slim
@@ -2788,13 +2883,15 @@ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
2788
2883
  - |
2789
2884
  ${indentBlock(docker9RouterEntrypointScript, 8)}
2790
2885
  environment:
2791
- - PORT=20128
2886
+ - PORT=${routerPort}
2792
2887
  - HOSTNAME=0.0.0.0
2793
2888
  - CI=true
2794
2889
  volumes:
2795
2890
  - 9router-data:/root/.9router
2891
+ - ./sync.js:/tmp/sync.js:ro
2892
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
2796
2893
  ports:
2797
- - "20128:20128"
2894
+ - "${routerPort}:${routerPort}"
2798
2895
 
2799
2896
  volumes:
2800
2897
  9router-data:`;
@@ -2807,11 +2904,11 @@ services:
2807
2904
  container_name: ${multiAppContainerName}
2808
2905
  restart: always
2809
2906
  env_file:
2810
- - .env
2907
+ - ../../.env
2811
2908
  ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
2812
2909
  - ${volumeMount}
2813
2910
  ports:
2814
- - "18791:18791"
2911
+ - "${gatewayPort}:${gatewayPort}"
2815
2912
 
2816
2913
  ollama:
2817
2914
  image: ollama/ollama:latest
@@ -2847,11 +2944,11 @@ services:
2847
2944
  container_name: ${multiAppContainerName}
2848
2945
  restart: always
2849
2946
  env_file:
2850
- - .env
2947
+ - ../../.env
2851
2948
  ${appEnvironmentBlock}${extraHosts} volumes:
2852
2949
  - ${volumeMount}
2853
2950
  ports:
2854
- - "18791:18791"`;
2951
+ - "${gatewayPort}:${gatewayPort}"`;
2855
2952
  }
2856
2953
  } else if (is9Router) {
2857
2954
  compose = `name: ${singleComposeName}
@@ -2861,13 +2958,14 @@ services:
2861
2958
  container_name: ${singleAppContainerName}
2862
2959
  restart: always
2863
2960
  env_file:
2864
- - .env
2961
+ - ../../.env
2865
2962
  depends_on:
2866
2963
  - 9router
2867
2964
  ${appEnvironmentBlock}${hasBrowser ? `${extraHostsBlock}\n` : ''} volumes:
2868
2965
  - ${volumeMount}
2966
+ - openclaw-plugins:/root/project/.openclaw/npm
2869
2967
  ports:
2870
- - "18791:18791"
2968
+ - "${gatewayPort}:${gatewayPort}"
2871
2969
 
2872
2970
  9router:
2873
2971
  image: node:22-slim
@@ -2879,16 +2977,19 @@ ${appEnvironmentBlock}${hasBrowser ? `${extraHostsBlock}\n` : ''} volumes:
2879
2977
  - |
2880
2978
  ${indentBlock(docker9RouterEntrypointScript, 8)}
2881
2979
  environment:
2882
- - PORT=20128
2980
+ - PORT=${routerPort}
2883
2981
  - HOSTNAME=0.0.0.0
2884
2982
  - CI=true
2885
2983
  volumes:
2886
2984
  - 9router-data:/root/.9router
2985
+ - ./sync.js:/tmp/sync.js:ro
2986
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
2887
2987
  ports:
2888
- - "20128:20128"
2988
+ - "${routerPort}:${routerPort}"
2889
2989
 
2890
2990
  volumes:
2891
- 9router-data:`;
2991
+ 9router-data:
2992
+ openclaw-plugins:`;
2892
2993
  } else if (isLocal) {
2893
2994
  const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
2894
2995
  compose = `name: ${singleComposeName}
@@ -2897,12 +2998,12 @@ services:
2897
2998
  build: .
2898
2999
  container_name: ${singleAppContainerName}
2899
3000
  restart: always
2900
- env_file: .env
3001
+ env_file: ../../.env
2901
3002
  ${appEnvironmentBlock} depends_on:
2902
3003
  ollama:
2903
3004
  condition: service_healthy
2904
3005
  ${hasBrowser ? `${extraHostsBlock}\n` : ''} ports:
2905
- - "18791:18791"
3006
+ - "${gatewayPort}:${gatewayPort}"
2906
3007
  volumes:
2907
3008
  - ${volumeMount}
2908
3009
 
@@ -2940,17 +3041,19 @@ services:
2940
3041
  container_name: ${singleAppContainerName}
2941
3042
  restart: always
2942
3043
  env_file:
2943
- - .env
3044
+ - ../../.env
2944
3045
  ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
2945
3046
  - ${volumeMount}
2946
3047
  ports:
2947
- - "18791:18791"`;
3048
+ - "${gatewayPort}:${gatewayPort}"`;
2948
3049
  }
2949
3050
 
2950
3051
  return {
2951
3052
  dockerfile,
2952
3053
  compose,
3054
+ entrypointScript: runtimeScript,
2953
3055
  syncScript,
3056
+ patchScript,
2954
3057
  docker9RouterEntrypointScript,
2955
3058
  gatewayPatchCmd: buildGatewayPatchCmd(),
2956
3059
  };
@@ -2971,7 +3074,9 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
2971
3074
  Object.assign(exports, globalThis.__openclawDockerGen);
2972
3075
  }
2973
3076
 
2974
- // ── buildNativeScriptCtx and shared native runtime helpers (setup/generators/native-helpers-gen.js)
3077
+
3078
+
3079
+ // ── buildNativeScriptCtx and shared native runtime helpers (setup/generators/native-helpers-gen.js)
2975
3080
  // @ts-nocheck
2976
3081
  /* eslint-disable no-undef, no-unused-vars */
2977
3082
  /**
@@ -3023,15 +3128,101 @@ function buildNativeScriptCtx(options) {
3023
3128
  });
3024
3129
 
3025
3130
  function native9RouterSyncScriptContent() {
3026
- return `const fs=require('fs');
3027
- const path=require('path');
3028
- const INTERVAL=30000;
3029
- const p=path.join(process.env.DATA_DIR||'.9router','db.json');
3030
- const ROUTER='${globalThis.__openclawCommon.NINE_ROUTER_API_BASE_URL}';
3031
- const PM=${JSON.stringify(SMART_ROUTE_PROVIDER_MODELS)};
3032
- const PREF=${JSON.stringify(SMART_ROUTE_PROVIDER_ORDER)};
3033
- console.log('[sync-combo] 9Router sync loop started...');
3034
- const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.ok){console.log('[sync-combo] API not ready, retrying...');return;}const d=await res.json();const rawConnections=Array.isArray(d.connections)?d.connections:Array.isArray(d.providerConnections)?d.providerConnections:[];const a=[...new Set(rawConnections.filter(c=>c&&c.provider&&c.isActive!==false&&!c.disabled).map(c=>c.provider))];let db={};try{db=JSON.parse(fs.readFileSync(p,'utf8'));}catch{}if(!db.combos)db.combos=[];const removeSmartRoute=()=>{const next=db.combos.filter(x=>x.id!=='smart-route');if(next.length!==db.combos.length){db.combos=next;fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Removed smart-route (no active providers)');}};if(!a.length){removeSmartRoute();return;}a.sort((x,y)=>(PREF.indexOf(x)===-1?99:PREF.indexOf(x))-(PREF.indexOf(y)===-1?99:PREF.indexOf(y)));const m=a.flatMap(provider=>PM[provider]||[]);if(!m.length){removeSmartRoute();return;}const c={id:'smart-route',name:'smart-route',alias:'smart-route',models:m};const i=db.combos.findIndex(x=>x.id==='smart-route');if(i>=0){if(JSON.stringify(db.combos[i].models)!==JSON.stringify(c.models)){db.combos[i]=c;fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Updated smart-route: '+c.models.length+' models from: '+a.join(','));}}else{db.combos.push(c);fs.writeFileSync(p,JSON.stringify(db,null,2));console.log('[sync-combo] Created smart-route: '+c.models.length+' models from: '+a.join(','));}}catch(e){console.log('[sync-combo] Error:',e.message);}};setTimeout(sync,5000);setInterval(sync,INTERVAL);`;
3131
+ return `const fs = require('fs');
3132
+ const path = require('path');
3133
+ const INTERVAL = 30000;
3134
+ const DB_PATH = path.join(process.env.DATA_DIR || '.9router', 'db', 'data.sqlite');
3135
+ const PORT = process.env.PORT || 20128;
3136
+ const COMBO_NAME = 'smart-route';
3137
+ const API_BASE = \\\`http://localhost:\\\${PORT}\\\`;
3138
+
3139
+ function ensureSettings() {
3140
+ try {
3141
+ let Database;
3142
+ try {
3143
+ const cp = require('child_process');
3144
+ const npmRoot = cp.execSync('npm root -g').toString().trim();
3145
+ Database = require(path.join(npmRoot, '9router', 'node_modules', 'better-sqlite3'));
3146
+ } catch {
3147
+ try { Database = require('better-sqlite3'); } catch { return; }
3148
+ }
3149
+ const db = Database(DB_PATH);
3150
+ const existing = db.prepare("SELECT * FROM settings WHERE id = 1").get();
3151
+ if (!existing) {
3152
+ db.prepare("INSERT INTO settings (id, data) VALUES (1, ?)").run(JSON.stringify({ requireLogin: false }));
3153
+ } else {
3154
+ try {
3155
+ const data = JSON.parse(existing.data || '{}');
3156
+ if (data.requireLogin !== false) {
3157
+ data.requireLogin = false;
3158
+ db.prepare("UPDATE settings SET data = ? WHERE id = 1").run(JSON.stringify(data));
3159
+ }
3160
+ } catch {}
3161
+ }
3162
+ db.close();
3163
+ } catch (e) {}
3164
+ }
3165
+
3166
+ const sync = async () => {
3167
+ try {
3168
+ if (!fs.existsSync(DB_PATH)) return;
3169
+
3170
+ let existingCombo = null;
3171
+ try {
3172
+ const resp = await fetch(\\\`\\\${API_BASE}/api/combos\\\`);
3173
+ if (resp.status === 401) {
3174
+ ensureSettings();
3175
+ return;
3176
+ }
3177
+ const data = await resp.json();
3178
+ if (data.combos) {
3179
+ existingCombo = data.combos.find(c => c.name === COMBO_NAME);
3180
+ }
3181
+ } catch (e) { return; }
3182
+
3183
+ if (existingCombo) return;
3184
+
3185
+ let activeProviders = [];
3186
+ try {
3187
+ const resp = await fetch(\\\`\\\${API_BASE}/api/providers\\\`);
3188
+ const data = await resp.json();
3189
+ const conns = data.connections || data.providerConnections || [];
3190
+ activeProviders = [...new Set(
3191
+ conns.filter(c => c && c.provider && c.isActive !== false && !c.disabled).map(c => c.provider)
3192
+ )];
3193
+ } catch (e) { return; }
3194
+
3195
+ if (!activeProviders.length) return;
3196
+
3197
+ let models = [];
3198
+ try {
3199
+ const resp = await fetch(\\\`\\\${API_BASE}/api/models\\\`);
3200
+ const data = await resp.json();
3201
+ if (data.models && Array.isArray(data.models)) {
3202
+ models = data.models
3203
+ .filter(m => activeProviders.includes(m.provider))
3204
+ .filter(m => !/(embedding|image|tts|stt|audio|vision)/i.test(m.model))
3205
+ .map(m => m.fullModel);
3206
+ }
3207
+ models = [...new Set(models)];
3208
+ } catch (e) { return; }
3209
+
3210
+ if (!models.length) return;
3211
+
3212
+ try {
3213
+ await fetch(\\\`\\\${API_BASE}/api/combos\\\`, {
3214
+ method: 'POST',
3215
+ headers: { 'Content-Type': 'application/json' },
3216
+ body: JSON.stringify({ name: COMBO_NAME, models })
3217
+ });
3218
+ console.log('[sync-combo] Created smart-route with ' + models.length + ' models');
3219
+ } catch (e) {}
3220
+ } catch (e) {}
3221
+ };
3222
+
3223
+ if (fs.existsSync(DB_PATH)) ensureSettings();
3224
+ setTimeout(sync, 10000);
3225
+ setInterval(sync, INTERVAL);`;
3035
3226
  }
3036
3227
 
3037
3228
  function native9RouterServerEntryLookup() {
@@ -3177,12 +3368,16 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3177
3368
  list: multiBotAgentMetas.map((meta) => ({
3178
3369
  id: meta.agentId,
3179
3370
  name: meta.name,
3180
- workspace: '.openclaw/' + meta.workspaceDir,
3181
- agentDir: `agents/${meta.agentId}/agent`,
3182
3371
  model: { primary: state.config.model, fallbacks: [] },
3183
3372
  })),
3184
3373
  },
3185
- commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
3374
+ commands: {
3375
+ native: 'auto',
3376
+ nativeSkills: 'auto',
3377
+ restart: true,
3378
+ ownerDisplay: 'raw',
3379
+ ...(state.config.skills.includes('scheduler') ? { ownerAllowFrom: ['*'] } : {}),
3380
+ },
3186
3381
  bindings: multiBotAgentMetas.map((meta) => ({
3187
3382
  agentId: meta.agentId,
3188
3383
  match: { channel: 'telegram', accountId: meta.accountId },
@@ -3209,6 +3404,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3209
3404
  },
3210
3405
  tools: {
3211
3406
  profile: 'full',
3407
+ ...(state.config.skills.includes('scheduler') ? { alsoAllow: ['group:automation'] } : {}),
3212
3408
  exec: { host: 'gateway', security: 'full', ask: 'off' },
3213
3409
  agentToAgent: {
3214
3410
  enabled: true,
@@ -3229,12 +3425,11 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3229
3425
  }
3230
3426
  } : {}),
3231
3427
  gateway: {
3232
- port: 18791,
3428
+ port: 18789,
3233
3429
  mode: 'local',
3234
- bind: state.nativeOs === 'vps' ? 'custom' : 'loopback',
3235
- ...(state.nativeOs === 'vps' ? { customBindHost: '0.0.0.0' } : {}),
3430
+ bind: state.nativeOs === 'vps' ? 'lan' : 'loopback',
3236
3431
  controlUi: {
3237
- allowedOrigins: getGatewayAllowedOrigins(18791),
3432
+ allowedOrigins: getGatewayAllowedOrigins(18789),
3238
3433
  },
3239
3434
  auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3240
3435
  },
@@ -3299,7 +3494,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3299
3494
  };
3300
3495
  }
3301
3496
 
3302
- // ── botEnvContent, botConfigContent, botWorkspaceFiles, botFiles, helpers (setup/generators/config-gen.js)
3497
+ // ── botEnvContent, botConfigContent, botWorkspaceFiles, botFiles, helpers (setup/generators/config-gen.js)
3303
3498
  // @ts-nocheck
3304
3499
  /* eslint-disable no-undef, no-unused-vars */
3305
3500
  /**
@@ -3356,12 +3551,12 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3356
3551
  const bot = state.bots[botIndex] || {};
3357
3552
  const botName = bot.name || `Bot ${botIndex + 1}`;
3358
3553
  const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3359
- const basePort = 18791 + botIndex;
3554
+ const basePort = 18789 + botIndex;
3360
3555
  const groupId = state.groupId || '';
3361
3556
 
3362
3557
  // Force use global provider if proxy mode is chosen globally, else use bot specific provider
3363
3558
  const botProvider = (provider && provider.isProxy) ? provider : (PROVIDERS[bot.provider] || provider);
3364
- const actualModel = botProvider.isProxy ? provider.models[0].id : (bot.model || state.config.model);
3559
+ const actualModel = botProvider.isProxy ? 'smart-route' : (bot.model || state.config.model);
3365
3560
  const bcfg = globalThis.__openclawBotConfig;
3366
3561
 
3367
3562
  const cfg = bcfg.buildOpenclawJson({
@@ -3529,7 +3724,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3529
3724
  }));
3530
3725
  }
3531
3726
 
3532
- // ── generateZaloLoginBat, generateZaloLoginSh (unified) (setup/generators/zalo-login-gen.js)
3727
+ // ── generateZaloLoginBat, generateZaloLoginSh (unified) (setup/generators/zalo-login-gen.js)
3533
3728
  // @ts-nocheck
3534
3729
  /* eslint-disable no-undef, no-unused-vars */
3535
3730
  /**
@@ -3566,15 +3761,13 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3566
3761
  * @param {string} opts.projectDirVar - BAT var for project dir e.g. '%PROJECT_DIR%'
3567
3762
  * @param {string} opts.label - Unique BAT label suffix (avoid duplicate labels)
3568
3763
  * e.g. 'win', 'multi', 'combo'
3569
- * @param {boolean} [opts.useInstance] - Use --instance default flag (for multi-bot flows)
3764
+ * @param {boolean} [opts.useInstance] - Reserved for QR file suffix only.
3570
3765
  * @returns {string[]} Lines to push into the bat script
3571
3766
  */
3572
3767
  function generateZaloLoginBat(opts) {
3573
3768
  const { homeVar, projectDirVar, label = 'default', useInstance = false } = opts;
3574
3769
  const credPath = `${homeVar}\\credentials\\zalouser\\credentials.json`;
3575
- const loginCmd = useInstance
3576
- ? 'openclaw channels login --channel zalouser --instance default --verbose'
3577
- : 'openclaw channels login --channel zalouser --verbose';
3770
+ const loginCmd = 'openclaw channels login --channel zalouser --verbose';
3578
3771
  const contLabel = `:zalo_continue_${label}`;
3579
3772
  const retryLabel = `:retry_zalo_${label}`;
3580
3773
 
@@ -3614,15 +3807,13 @@ function generateZaloLoginBat(opts) {
3614
3807
  * @param {object} opts
3615
3808
  * @param {string} opts.homeVar - Shell var for OPENCLAW_HOME e.g. '$OPENCLAW_HOME'
3616
3809
  * @param {string} opts.projectDirVar - Shell var for project dir e.g. '$PROJECT_DIR'
3617
- * @param {boolean} [opts.useInstance] - Use --instance default flag
3810
+ * @param {boolean} [opts.useInstance] - Reserved for QR file suffix only.
3618
3811
  * @returns {string[]} Lines to push into the sh script
3619
3812
  */
3620
3813
  function generateZaloLoginSh(opts) {
3621
3814
  const { homeVar, projectDirVar, useInstance = false } = opts;
3622
3815
  const credPath = `${homeVar}/credentials/zalouser/credentials.json`;
3623
- const loginCmd = useInstance
3624
- ? 'openclaw channels login --channel zalouser --instance default --verbose'
3625
- : 'openclaw channels login --channel zalouser --verbose';
3816
+ const loginCmd = 'openclaw channels login --channel zalouser --verbose';
3626
3817
 
3627
3818
  return [
3628
3819
  `# ── Zalo Personal Login (idempotent) ─────────────────────────────────`,
@@ -3646,7 +3837,7 @@ function generateZaloLoginSh(opts) {
3646
3837
  ];
3647
3838
  }
3648
3839
 
3649
- // ── generateStartScript wizard wrapper (delegates to install-gen) (setup/generators/gateway-start-gen.js)
3840
+ // ── generateStartScript wizard wrapper (delegates to install-gen) (setup/generators/gateway-start-gen.js)
3650
3841
  // @ts-nocheck
3651
3842
  /* eslint-disable no-undef, no-unused-vars */
3652
3843
  /**
@@ -3693,7 +3884,7 @@ function generateStartScript() {
3693
3884
  return null;
3694
3885
  }
3695
3886
 
3696
- // ── generateUninstallScript, setup script download helpers (setup/generators/download-gen.js)
3887
+ // ── generateUninstallScript, setup script download helpers (setup/generators/download-gen.js)
3697
3888
  // @ts-nocheck
3698
3889
  /* eslint-disable no-undef, no-unused-vars */
3699
3890
  /**
@@ -3950,7 +4141,7 @@ window.__downloadGen = {
3950
4141
  updateDockerDlLabel,
3951
4142
  };
3952
4143
 
3953
- // ── Windows .bat — if (state.nativeOs === "win") block (setup/os/win-bat.js)
4144
+ // ── Windows .bat — if (state.nativeOs === "win") block (setup/os/win-bat.js)
3954
4145
  // @ts-nocheck
3955
4146
  /* eslint-disable no-undef, no-unused-vars */
3956
4147
  /**
@@ -4002,8 +4193,8 @@ function generateWinBat(ctx) {
4002
4193
 
4003
4194
  function appendDashboardInfo(arr) {
4004
4195
  arr.push('echo.');
4005
- arr.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
4006
- arr.push('echo Other reachable URLs: http://localhost:18791');
4196
+ arr.push('echo OpenClaw Dashboard: http://127.0.0.1:18789');
4197
+ arr.push('echo Other reachable URLs: http://localhost:18789');
4007
4198
  arr.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
4008
4199
  if (is9Router) {
4009
4200
  arr.push('echo.');
@@ -4095,7 +4286,7 @@ function generateWinBat(ctx) {
4095
4286
  return { scriptName, scriptContent };
4096
4287
  }
4097
4288
 
4098
- // ── macOS .sh — if (state.nativeOs === "macos") block (setup/os/macos-sh.js)
4289
+ // ── macOS .sh — if (state.nativeOs === "macos") block (setup/os/macos-sh.js)
4099
4290
  // @ts-nocheck
4100
4291
  /* eslint-disable no-undef, no-unused-vars */
4101
4292
  /**
@@ -4213,7 +4404,7 @@ function generateMacOsSh(ctx) {
4213
4404
  return { scriptName, scriptContent };
4214
4405
  }
4215
4406
 
4216
- // ── VPS/PM2 .sh — if (state.nativeOs === "vps") block (setup/os/vps-sh.js)
4407
+ // ── VPS/PM2 .sh — if (state.nativeOs === "vps") block (setup/os/vps-sh.js)
4217
4408
  // @ts-nocheck
4218
4409
  /* eslint-disable no-undef, no-unused-vars */
4219
4410
  /**
@@ -4327,7 +4518,7 @@ GWEOF`);
4327
4518
  }
4328
4519
 
4329
4520
  vps.push('echo ""');
4330
- vps.push('echo "Dashboard: http://127.0.0.1:18791"');
4521
+ vps.push('echo "Dashboard: http://127.0.0.1:18789"');
4331
4522
  if (is9Router) vps.push('echo "9Router: http://127.0.0.1:20128/dashboard"');
4332
4523
  vps.push('echo ""');
4333
4524
  vps.push(`echo "Restart: bash start-bot.sh"`);
@@ -4338,7 +4529,7 @@ GWEOF`);
4338
4529
  return { scriptName, scriptContent };
4339
4530
  }
4340
4531
 
4341
- // ── Linux Desktop .sh — if (state.nativeOs === "linux-desktop") block (setup/os/linux-sh.js)
4532
+ // ── Linux Desktop .sh — if (state.nativeOs === "linux-desktop") block (setup/os/linux-sh.js)
4342
4533
  // @ts-nocheck
4343
4534
  /* eslint-disable no-undef, no-unused-vars */
4344
4535
  /**
@@ -4401,7 +4592,7 @@ function generateLinuxSh(ctx) {
4401
4592
  return { scriptName, scriptContent };
4402
4593
  }
4403
4594
 
4404
- // ── UI init, language/channel/deploy controllers, form rendering (setup/ui/controller.js)
4595
+ // ── UI init, language/channel/deploy controllers, form rendering (setup/ui/controller.js)
4405
4596
  // @ts-nocheck
4406
4597
  /* eslint-disable no-undef, no-unused-vars */
4407
4598
  /**
@@ -5050,7 +5241,7 @@ function generateLinuxSh(ctx) {
5050
5241
  envContent.textContent = lines.join('\n');
5051
5242
  }
5052
5243
 
5053
- // ── Multi-bot state + UI (setup/ui/multi-bot.js) ───────────────────
5244
+ // ── Multi-bot state + UI (setup/ui/multi-bot.js) ───────────────────
5054
5245
  // @ts-nocheck
5055
5246
  /* eslint-disable no-undef, no-unused-vars */
5056
5247
  /**
@@ -5399,7 +5590,7 @@ function generateLinuxSh(ctx) {
5399
5590
 
5400
5591
  // ========== Step 1: Deploy Mode + OS ==========
5401
5592
 
5402
- // ── Step navigation, validation (setup/ui/steps.js) ────────────────
5593
+ // ── Step navigation, validation (setup/ui/steps.js) ────────────────
5403
5594
  // @ts-nocheck
5404
5595
  /* eslint-disable no-undef, no-unused-vars */
5405
5596
  /**
@@ -5559,7 +5750,7 @@ function generateLinuxSh(ctx) {
5559
5750
 
5560
5751
  // ========== Step 2: Bot Config ==========
5561
5752
 
5562
- // ── generateOutput + generateNativeScript + clipboard (setup/ui/output.js)
5753
+ // ── generateOutput + generateNativeScript + clipboard (setup/ui/output.js)
5563
5754
  // @ts-nocheck
5564
5755
  /* eslint-disable no-undef, no-unused-vars */
5565
5756
  /**
@@ -5726,15 +5917,26 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5726
5917
  },
5727
5918
  list: [{
5728
5919
  id: agentId,
5729
- workspace: `.openclaw/workspace-${agentId}`,
5920
+ name: botName,
5921
+ workspace: `/root/project/.openclaw/workspace-${agentId}`,
5730
5922
  agentDir: `agents/${agentId}/agent`,
5731
5923
  model: { primary: state.config.model, fallbacks: [] },
5732
5924
  }],
5733
5925
  },
5734
- commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
5926
+ commands: {
5927
+ native: 'auto',
5928
+ nativeSkills: 'auto',
5929
+ restart: true,
5930
+ ownerDisplay: 'raw',
5931
+ ...(state.config.skills.includes('scheduler') ? { ownerAllowFrom: ['*'] } : {}),
5932
+ },
5735
5933
  channels: ch.channelConfig,
5736
- tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
5737
- gateway: common.buildGatewayConfig(18791, 'native', getGatewayAllowedOrigins(18791)),
5934
+ tools: {
5935
+ profile: 'full',
5936
+ ...(state.config.skills.includes('scheduler') ? { alsoAllow: ['group:automation'] } : {}),
5937
+ exec: { host: 'gateway', security: 'full', ask: 'off' },
5938
+ },
5939
+ gateway: common.buildGatewayConfig(18789, state.deployMode, getGatewayAllowedOrigins(18789), state.nativeOs || ''),
5738
5940
  };
5739
5941
 
5740
5942
  // 9Router: add proxy endpoint config under models.providers
@@ -5824,7 +6026,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5824
6026
  clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
5825
6027
  id: meta.agentId,
5826
6028
  name: meta.name,
5827
- workspace: '.openclaw/' + meta.workspaceDir,
6029
+ workspace: `/root/project/.openclaw/${meta.workspaceDir}`,
5828
6030
  agentDir: `agents/${meta.agentId}/agent`,
5829
6031
  model: { primary: state.config.model, fallbacks: [] },
5830
6032
  }));
@@ -5855,6 +6057,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5855
6057
  };
5856
6058
  clawConfig.tools = {
5857
6059
  ...(clawConfig.tools || {}),
6060
+ ...(state.config.skills.includes('scheduler') ? { alsoAllow: ['group:automation'] } : {}),
5858
6061
  agentToAgent: {
5859
6062
  enabled: true,
5860
6063
  allow: multiBotAgentMetas.map((meta) => meta.agentId),
@@ -5949,6 +6152,9 @@ model:
5949
6152
  dockerfileSkillInstallMode: 'build',
5950
6153
  runtimeCommandParts: [
5951
6154
  pluginInstallCmd,
6155
+ // zalouser: use npm install (not openclaw plugins install) to avoid openclaw.json writes
6156
+ // ClawHub build gives error:not configured; npm version works correctly
6157
+ state.channel === 'zalo-personal' ? 'ensure_zalouser' : '',
5952
6158
  'while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done >/dev/null 2>&1 &'
5953
6159
  ].filter(Boolean),
5954
6160
  plainSingleExtraHosts: true,
@@ -5958,6 +6164,7 @@ model:
5958
6164
  });
5959
6165
  const dockerfile = dockerArtifacts.dockerfile;
5960
6166
  const compose = dockerArtifacts.compose;
6167
+ const entrypointScript = dockerArtifacts.entrypointScript;
5961
6168
  // isMultiBot => unified into isMultiBot above
5962
6169
  setOutput('out-dockerfile', dockerfile);
5963
6170
  setOutput('out-compose', compose);
@@ -6371,6 +6578,7 @@ fi
6371
6578
  if (!isNativeMode) {
6372
6579
  sharedFiles['docker/openclaw/Dockerfile'] = dockerfile;
6373
6580
  sharedFiles['docker/openclaw/docker-compose.yml'] = compose;
6581
+ sharedFiles['docker/openclaw/entrypoint.sh'] = entrypointScript;
6374
6582
  sharedFiles['docker/openclaw/.env'] = rootEnvContent;
6375
6583
  }
6376
6584
  sharedFiles[globalThis.__openclawCommon.TELEGRAM_SETUP_GUIDE_FILENAME] = buildTelegramPostInstallChecklist();
@@ -6442,6 +6650,7 @@ fi
6442
6650
  if (!isNativeMode) {
6443
6651
  singleFiles['docker/openclaw/Dockerfile'] = dockerfile;
6444
6652
  singleFiles['docker/openclaw/docker-compose.yml'] = compose;
6653
+ singleFiles['docker/openclaw/entrypoint.sh'] = entrypointScript;
6445
6654
  singleFiles['docker/openclaw/.env'] = rootEnvContent;
6446
6655
  }
6447
6656
  state._generatedFiles = singleFiles;
@@ -6610,38 +6819,46 @@ fi
6610
6819
  // ========== Zalo Personal Login Guide (post-setup) ==========
6611
6820
  function generateZaloOnboardGuide() {
6612
6821
  const lang = document.getElementById('cfg-language')?.value || 'vi';
6613
- setOutput('out-zalo-onboard-cmd', `docker compose stop ai-bot
6614
- docker compose run --rm --no-deps ai-bot openclaw channels login --channel zalouser --verbose
6615
- docker compose up -d --force-recreate ai-bot
6616
- docker compose exec ai-bot openclaw channels status --probe`);
6822
+ const isVi = lang === 'vi';
6823
+ const containerName = state.botCount > 1 ? 'openclaw-multibot' : 'openclaw-bot';
6617
6824
 
6618
- if (lang === 'vi') {
6825
+ setOutput('out-zalo-onboard-cmd', `# ${isVi ? 'Bước 1: Dọn dẹp session cũ' : 'Step 1: Clean up old session'}
6826
+ docker exec ${containerName} rm -f /root/project/.openclaw/credentials/zalouser/credentials.json
6827
+
6828
+ # ${isVi ? 'Bước 2: Kích hoạt màn hình login QR (Quét mã trên terminal)' : 'Step 2: Start login QR (Scan on terminal)'}
6829
+ docker exec -it ${containerName} openclaw channels login --channel zalouser --verbose
6830
+
6831
+ # ${isVi ? 'Bước 3: Khởi động lại container sau khi login thành công' : 'Step 3: Restart container after successful login'}
6832
+ docker restart ${containerName}`);
6833
+
6834
+ if (isVi) {
6619
6835
  setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
6620
- │ Chạy lệnh bên trái để OpenClaw tạo QR đăng nhập.
6836
+ │ Chạy các lệnh bên trái để đăng nhập Zalo Personal
6837
+ │ theo quy trình chuẩn 4 bước. │
6621
6838
  ├─────────────────────────────────────────────────────┤
6622
- │ 1. Đảm bảo container/gateway đã chạy xong.
6623
- 2. Stop ai-bot, login bằng compose run one-shot.
6624
- 3. OpenClaw sẽ in ra đường dẫn file QR trong /tmp.
6625
- 4. Copy file QR ra ngoài nếu cần:
6626
- │ docker cp openclaw-bot:/tmp/openclaw/ │
6839
+ │ 1. Lệnh 1 xoá file credentials.json cũ để tránh
6840
+ lỗi xung đột "Already linked".
6841
+ 2. Lệnh 2 mở màn hình login. Quét QR hiện trên
6842
+ terminal hoặc lấy file từ container:
6843
+ │ docker cp ${containerName}:/tmp/openclaw/ │
6627
6844
  │ openclaw-zalouser-qr-default.png . │
6628
- 5. Mở ảnh QR → quét bằng app Zalo xác nhận.
6629
- 6. Start lại: docker compose up -d --force-recreate
6630
- 7. Chạy channels status --probe, phải thấy running.│
6845
+ 3. Sau khi quét xong terminal báo thành công,
6846
+ nhấn Ctrl+C để thoát.
6847
+ 4. Chạy Lệnh 3 để restart container giúp nhận tin. │
6631
6848
  └─────────────────────────────────────────────────────┘`);
6632
6849
  } else {
6633
6850
  setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
6634
- │ Run the command on the left to generate a Zalo QR.
6851
+ │ Run the commands on the left to login Zalo Personal
6852
+ │ following the standard 4-step workflow. │
6635
6853
  ├─────────────────────────────────────────────────────┤
6636
- │ 1. Make sure the container/gateway is already up.
6637
- 2. Stop ai-bot; login with compose run one-shot.
6638
- 3. OpenClaw prints the QR image path under /tmp.
6639
- 4. Copy the QR out if needed:
6640
- │ docker cp openclaw-bot:/tmp/openclaw/ │
6854
+ │ 1. Command 1 deletes the old credentials.json file
6855
+ to avoid "Already linked" conflicts.
6856
+ 2. Command 2 opens the login interface. Scan the
6857
+ QR on your terminal or copy the image file:
6858
+ │ docker cp ${containerName}:/tmp/openclaw/ │
6641
6859
  │ openclaw-zalouser-qr-default.png . │
6642
- 5. Open the image scan with Zalo mobile app.
6643
- 6. Start again: docker compose up -d --force-re...
6644
- │ 7. Run channels status --probe; it should run. │
6860
+ 3. Once scanned and successful, press Ctrl+C.
6861
+ 4. Run Command 3 to restart the container.
6645
6862
  └─────────────────────────────────────────────────────┘`);
6646
6863
  }
6647
6864
  }