create-openclaw-bot 5.7.10 → 5.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  /**
@@ -406,6 +406,13 @@
406
406
  */
407
407
  // ========== Available Plugins (npm packages — runtime/channel extensions) ==========
408
408
  const PLUGINS = [
409
+ {
410
+ id: 'browser-automation',
411
+ name: 'Browser Automation ⭐',
412
+ icon: '🌐',
413
+ descVi: 'Smart Search + Điều khiển trình duyệt Chrome/Chromium (ẩn & thật)', descEn: 'Smart Search + Chrome/Chromium browser control (headless & real)',
414
+ package: 'openclaw-browser-automation',
415
+ },
409
416
  {
410
417
  id: 'telegram-multibot-relay',
411
418
  name: 'Telegram Multi-Bot Relay',
@@ -445,7 +452,7 @@
445
452
  ];
446
453
 
447
454
 
448
- // ── SKILLS list (setup/data/skills.js) ─────────────────────────────
455
+ // ── SKILLS list (setup/data/skills.js) ─────────────────────────────
449
456
  // @ts-nocheck
450
457
  /* eslint-disable no-undef, no-unused-vars */
451
458
  /**
@@ -468,14 +475,6 @@
468
475
  */
469
476
  // ========== Available Skills (ClawHub registry — agent capabilities) ==========
470
477
  const SKILLS = [
471
- {
472
- id: 'browser',
473
- name: 'Browser Automation ⭐(Khuyên dùng)',
474
- icon: '🌐',
475
- descVi: 'Tự động thao tác trình duyệt (Playwright)', descEn: 'Automated browser control (Playwright)',
476
- slug: 'browser-automation',
477
- noteVi: 'Cần bật Chrome Debug Mode trên máy host', noteEn: 'Requires Chrome Debug Mode on host',
478
- },
479
478
  {
480
479
  id: 'memory',
481
480
  name: 'Long-term Memory ⭐(Khuyên dùng)',
@@ -574,7 +573,7 @@
574
573
  className: 'plugin-card__badge plugin-card__badge--recommended'
575
574
  };
576
575
  }
577
- if (skill.id === 'browser' || skill.id === 'scheduler') {
576
+ if (skill.id === 'scheduler') {
578
577
  return {
579
578
  text: isVi ? 'Khuyên dùng' : 'Recommended',
580
579
  className: 'plugin-card__badge plugin-card__badge--recommended'
@@ -616,10 +615,10 @@
616
615
  }
617
616
 
618
617
 
619
- // ── Shared runtime constants, relay helpers, auth profile builders (setup/shared/common-gen.js)
618
+ // ── Shared runtime constants, relay helpers, auth profile builders (setup/shared/common-gen.js)
620
619
  // @ts-nocheck
621
620
  (function (root) {
622
- const OPENCLAW_NPM_SPEC = 'openclaw@2026.5.4';
621
+ const OPENCLAW_NPM_SPEC = 'openclaw@latest';
623
622
  const OPENCLAW_RUNTIME_PACKAGES = 'grammy @grammyjs/runner @grammyjs/transformer-throttler @buape/carbon @larksuiteoapi/node-sdk @slack/web-api';
624
623
  const NINE_ROUTER_NPM_SPEC = '9router@latest';
625
624
  const NINE_ROUTER_PORT = 20128;
@@ -861,8 +860,9 @@ If setup reported a plugin install error, run this after the bot is running:
861
860
  return JSON.stringify(buildAuthProfilesJson(options), null, 2);
862
861
  }
863
862
 
864
- function get9RouterBaseUrl(deployMode = 'native') {
865
- return deployMode === 'docker' ? `${NINE_ROUTER_DOCKER_API_BASE_URL}/v1` : `${NINE_ROUTER_API_BASE_URL}/v1`;
863
+ function get9RouterBaseUrl(deployMode = 'native', routerPort) {
864
+ const port = routerPort || NINE_ROUTER_PORT;
865
+ return deployMode === 'docker' ? `http://9router:${port}/v1` : `http://localhost:${port}/v1`;
866
866
  }
867
867
 
868
868
  function build9RouterProviderConfig(baseUrl = `${NINE_ROUTER_API_BASE_URL}/v1`) {
@@ -870,6 +870,9 @@ If setup reported a plugin install error, run this after the bot is running:
870
870
  baseUrl,
871
871
  apiKey: NINE_ROUTER_PROXY_API_KEY,
872
872
  api: 'openai-completions',
873
+ request: {
874
+ allowPrivateNetwork: true,
875
+ },
873
876
  models: [
874
877
  {
875
878
  id: 'smart-route',
@@ -877,18 +880,12 @@ If setup reported a plugin install error, run this after the bot is running:
877
880
  contextWindow: 200000,
878
881
  maxTokens: 8192,
879
882
  },
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
883
  ],
887
884
  };
888
885
  }
889
886
 
890
- function buildGatewayConfig(port = 18791, deployMode = 'native', allowedOrigins = [], osChoice = '') {
891
- const normalizedPort = Number(port) || 18791;
887
+ function buildGatewayConfig(port = 18789, deployMode = 'native', allowedOrigins = [], osChoice = '') {
888
+ const normalizedPort = Number(port) || 18789;
892
889
  const cfg = {
893
890
  port: normalizedPort,
894
891
  mode: 'local',
@@ -933,7 +930,7 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
933
930
  Object.assign(exports, globalThis.__openclawCommon);
934
931
  }
935
932
 
936
- // ── Shared workspace file builders (IDENTITY, SOUL, AGENTS, TOOLS, TEAMS...) (setup/shared/workspace-gen.js)
933
+ // ── Shared workspace file builders (IDENTITY, SOUL, AGENTS, TOOLS, TEAMS...) (setup/shared/workspace-gen.js)
937
934
  /** @typedef {typeof globalThis & { __openclawWorkspace?: Record<string, Function> }} OpenClawWorkspaceRoot */
938
935
 
939
936
  const workspaceRoot = /** @type {OpenClawWorkspaceRoot} */ (
@@ -1061,6 +1058,226 @@ function buildBootstrapDoc(options = {}) {
1061
1058
  : `# BOOTSTRAP\n\n## Startup role\n- You are **${botName}**.\n- Your identity, role, and style are already defined in **IDENTITY.md**, **SOUL.md**, and **AGENTS.md**.\n- Use those files as the highest-priority source of truth for your persona in this workspace.\n\n## Mandatory\n- Do not ask the user to redefine your name, vibe, persona, signature emoji, or assistant style.\n- Do not say you just woke up, just came online, are choosing your identity, or any similar onboarding line.\n- Do not behave like a blank freshly-created bot.\n- If the user only sends a short opener like "hi" or "alo", greet briefly and reply in your existing role.\n\n## Telegram\n- Before EVERY user-visible Telegram reply, if the current turn exposes a native reaction tool/action, use it to add \`👍\` first unless \`👍\` is already present.\n- Never claim that you reacted unless the tool actually ran and the reaction is real on the chat.\n- If the target chat does not support real reactions or the tool is not exposed, still reply normally and do not mention the technical limitation.\n`;
1062
1059
  }
1063
1060
 
1061
+ function buildSearchToolJs() {
1062
+ return `/**
1063
+ * search-tool.js — Stealth search via Playwright Headless Chromium or CDP fallback
1064
+ * Zero tokens, no API keys, concurrent multi-engine scraping (Google + Bing + DuckDuckGo).
1065
+ * Usage: node search-tool.js "<query>" [limit]
1066
+ */
1067
+ let playwright;
1068
+ try {
1069
+ playwright = require('playwright-core');
1070
+ } catch (e) {
1071
+ try {
1072
+ playwright = require('/usr/local/lib/node_modules/openclaw/node_modules/playwright-core');
1073
+ } catch (err) {
1074
+ try {
1075
+ const path = require('path');
1076
+ playwright = require(path.join(process.cwd(), 'node_modules', 'playwright-core'));
1077
+ } catch (x) {
1078
+ console.error(JSON.stringify({ error: 'Playwright not found! Install it or run within OpenClaw environment.' }));
1079
+ process.exit(1);
1080
+ }
1081
+ }
1082
+ }
1083
+ const { chromium } = playwright;
1084
+
1085
+ const query = process.argv[2];
1086
+ const limit = parseInt(process.argv[3]) || 5;
1087
+ const CDP_URL = 'http://127.0.0.1:9222';
1088
+
1089
+ if (!query) {
1090
+ console.error(JSON.stringify({ error: 'Usage: node search-tool.js "<query>" [limit]' }));
1091
+ process.exit(1);
1092
+ }
1093
+
1094
+ (async () => {
1095
+ let browser;
1096
+ let ctx;
1097
+ let isStandalone = false;
1098
+ try {
1099
+ // Try connecting to active Chrome CDP first
1100
+ try {
1101
+ browser = await chromium.connectOverCDP(CDP_URL, { timeout: 3000 });
1102
+ ctx = browser.contexts()[0];
1103
+ } catch (e) {
1104
+ // Fallback to standalone headless Chromium launch
1105
+ browser = await chromium.launch({
1106
+ headless: true,
1107
+ args: [
1108
+ '--no-sandbox',
1109
+ '--disable-gpu',
1110
+ '--disable-dev-shm-usage',
1111
+ '--disable-blink-features=AutomationControlled'
1112
+ ]
1113
+ });
1114
+ isStandalone = true;
1115
+ ctx = await browser.newContext({
1116
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
1117
+ });
1118
+ }
1119
+
1120
+ // Run search queries concurrently on three search engines
1121
+ const [googleResults, bingResults, ddgResults] = await Promise.all([
1122
+ // Google
1123
+ (async () => {
1124
+ const page = await ctx.newPage();
1125
+ try {
1126
+ await page.goto('https://www.google.com/search?q=' + encodeURIComponent(query) + '&hl=vi', { waitUntil: 'domcontentloaded', timeout: 10000 });
1127
+ const res = await page.evaluate(() => {
1128
+ const list = [];
1129
+ const links = Array.from(document.querySelectorAll('a h3'));
1130
+ for (const head of links) {
1131
+ const a = head.closest('a');
1132
+ if (!a) continue;
1133
+ const url = a.href;
1134
+ const title = head.textContent || '';
1135
+ let snippet = '';
1136
+ let parent = a.parentElement;
1137
+ while (parent && parent.tagName !== 'DIV') {
1138
+ parent = parent.parentElement;
1139
+ }
1140
+ if (parent) {
1141
+ const descEl = parent.parentElement?.querySelector('.VwiC3b, .yHGvwa, div[style*="-webkit-line-clamp"]');
1142
+ if (descEl) {
1143
+ snippet = descEl.textContent || '';
1144
+ } else {
1145
+ const texts = Array.from(parent.parentElement?.querySelectorAll('div, span') || [])
1146
+ .map(el => el.textContent.trim())
1147
+ .filter(txt => txt.length > 30 && !txt.includes(title));
1148
+ if (texts.length > 0) snippet = texts[0];
1149
+ }
1150
+ }
1151
+ if (url && title) {
1152
+ list.push({ title, url, snippet });
1153
+ }
1154
+ }
1155
+ return list;
1156
+ });
1157
+ await page.close();
1158
+ return res;
1159
+ } catch (e) {
1160
+ if (page) await page.close();
1161
+ return [];
1162
+ }
1163
+ })(),
1164
+
1165
+ // Bing
1166
+ (async () => {
1167
+ const page = await ctx.newPage();
1168
+ try {
1169
+ await page.goto('https://www.bing.com/search?q=' + encodeURIComponent(query), { waitUntil: 'domcontentloaded', timeout: 10000 });
1170
+ const res = await page.evaluate(() => {
1171
+ const list = [];
1172
+ const items = document.querySelectorAll('li.b_algo');
1173
+ for (const item of items) {
1174
+ const titleEl = item.querySelector('h2 a');
1175
+ if (!titleEl) continue;
1176
+ const title = titleEl.textContent || '';
1177
+ const url = titleEl.href;
1178
+ let snippet = '';
1179
+ const snippetEl = item.querySelector('.b_caption p, .b_snippet, p');
1180
+ if (snippetEl) {
1181
+ snippet = snippetEl.textContent || '';
1182
+ }
1183
+ if (url && title) {
1184
+ list.push({ title, url, snippet });
1185
+ }
1186
+ }
1187
+ return list;
1188
+ });
1189
+ await page.close();
1190
+ return res;
1191
+ } catch (e) {
1192
+ if (page) await page.close();
1193
+ return [];
1194
+ }
1195
+ })(),
1196
+
1197
+ // DuckDuckGo
1198
+ (async () => {
1199
+ const page = await ctx.newPage();
1200
+ try {
1201
+ await page.goto('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query), { waitUntil: 'domcontentloaded', timeout: 10000 });
1202
+ const res = await page.evaluate(() => {
1203
+ const list = [];
1204
+ const elements = document.querySelectorAll('.result');
1205
+ for (const el of elements) {
1206
+ const titleEl = el.querySelector('.result__title a');
1207
+ const snippetEl = el.querySelector('.result__snippet');
1208
+ if (titleEl) {
1209
+ list.push({
1210
+ title: titleEl.textContent.trim(),
1211
+ url: titleEl.href,
1212
+ snippet: snippetEl ? snippetEl.textContent.trim() : ''
1213
+ });
1214
+ }
1215
+ }
1216
+ return list;
1217
+ });
1218
+ await page.close();
1219
+ return res;
1220
+ } catch (e) {
1221
+ if (page) await page.close();
1222
+ return [];
1223
+ }
1224
+ })()
1225
+ ]);
1226
+
1227
+ // Deduplicate results by normalized URL
1228
+ const allResults = [...googleResults, ...bingResults, ...ddgResults];
1229
+ const uniqueResults = [];
1230
+ const seenUrls = new Set();
1231
+ for (const res of allResults) {
1232
+ if (!res.url || !res.title) continue;
1233
+ let normUrl = res.url.replace(/^(https?:\\/\\/)?(www\\.)?/, '').toLowerCase();
1234
+ if (normUrl.endsWith('/')) normUrl = normUrl.slice(0, -1);
1235
+ if (!seenUrls.has(normUrl)) {
1236
+ seenUrls.add(normUrl);
1237
+ uniqueResults.push(res);
1238
+ }
1239
+ }
1240
+
1241
+ // Score results to prioritize numeric price data for financial queries
1242
+ const isPriceQuery = /giá|vàng|đô|usd|sjc|sh|hôm nay|price|gold|rate|vnd|xe|vnđ/i.test(query);
1243
+ const scoredResults = uniqueResults.map(res => {
1244
+ let score = 0;
1245
+ // Base length score
1246
+ score += Math.min(res.snippet.length / 50, 5);
1247
+
1248
+ if (isPriceQuery) {
1249
+ // Number density check
1250
+ const numCount = (res.snippet.match(/\\d+/g) || []).length;
1251
+ score += Math.min(numCount * 2, 10);
1252
+
1253
+ // Priority keywords boost
1254
+ if (/lượng|chỉ|triệu|nghìn|vnd|usd|sjc|xe|bán|mua|giá/i.test(res.snippet)) {
1255
+ score += 8;
1256
+ }
1257
+ }
1258
+ return { ...res, score };
1259
+ });
1260
+
1261
+ // Sort by score desc
1262
+ scoredResults.sort((a, b) => b.score - a.score);
1263
+
1264
+ // Map back to output format and limit
1265
+ const output = scoredResults.map(({ score, ...rest }) => rest).slice(0, limit);
1266
+ console.log(JSON.stringify(output, null, 2));
1267
+
1268
+ } catch (err) {
1269
+ console.error(JSON.stringify({ error: err.message }));
1270
+ } finally {
1271
+ if (browser && isStandalone) {
1272
+ try {
1273
+ await browser.close();
1274
+ } catch(e) {}
1275
+ }
1276
+ }
1277
+ })();
1278
+ `;
1279
+ }
1280
+
1064
1281
  function buildBrowserToolJs(variant = 'wizard') {
1065
1282
  // v2: Full-featured browser-tool.js matching OpenClaw native browser plugin capabilities
1066
1283
  // Both 'cli' and 'wizard' variants now use the same full script
@@ -1187,8 +1404,18 @@ const CDP_URL = 'http://127.0.0.1:9222';
1187
1404
  const { isVi = true, variant = 'wizard', workspaceRoot = '' } = options;
1188
1405
  const wsRoot = workspaceRoot.replace(/\/+$/, '');
1189
1406
  const btPath = wsRoot ? `${wsRoot}/browser-tool.js` : 'browser-tool.js';
1407
+ let modeHeading = '';
1408
+ if (variant === 'cli-server') {
1409
+ modeHeading = isVi
1410
+ ? `# 🌍 Trình duyệt ảo (Browser Automation)\n\n## 💡 Hướng dẫn vận hành:\n- **Script điều khiển:** \`browser-tool.js\` (Mọi câu lệnh browser đều chạy qua script này).\n- **Môi trường chạy:**\n - **Trên VPS / Linux Server (Headless):** Trình duyệt chạy ngầm hoàn toàn độc lập (Headless) bên trong Docker / Server qua Xvfb. Không thể mở màn hình Chrome thật.\n - **Trên Máy tính cá nhân (Windows/Mac) - Dù chạy Docker hay Native:**\n - **Mặc định:** Chạy ngầm (headless) cực kỳ ổn định.\n - **Chế độ quan sát (Xem bot click):** Nếu bạn muốn xem trực tiếp Chrome thật hoạt động trên màn hình, hãy chạy file \`start-chrome-debug.bat\` (trên Windows) hoặc \`start-chrome-debug.sh\` (trên Mac) ở máy của bạn **trước khi** bot kết nối! Bot sẽ tự động chuyển sang điều khiển màn hình Chrome thật của bạn.\n- **Kết nối mặc định:** \`http://127.0.0.1:9222\`\n\n`
1411
+ : `# 🌍 Browser Automation\n\n## 💡 Operating Guide:\n- **Control script:** \`browser-tool.js\` (All browser commands are executed through this script).\n- **Running environment:**\n - **On VPS / Linux Server (Headless Server Mode):** The browser runs fully headless and isolated inside Docker / Server via Xvfb. No GUI Chrome can be launched.\n - **On Personal Computers (Windows/Mac) - Docker or Native:**\n - **Default:** Runs headless and stable in the background.\n - **Observer Mode (Visual Chrome GUI):** If you want to see the real Chrome window being controlled, run \`start-chrome-debug.bat\` (on Windows) or \`start-chrome-debug.sh\` (on Mac) on your host machine **before** the bot connects! The bot will automatically hook into your real desktop Chrome.\n- **Default endpoint:** \`http://127.0.0.1:9222\`\n\n`;
1412
+ } else {
1413
+ modeHeading = isVi
1414
+ ? `# 🌍 Hướng dẫn Browser (Chrome CDP)\n- **Script điều khiển:** \`browser-tool.js\`\n- **Kết nối Chrome debug:** \`http://127.0.0.1:9222\`\n- **Xem trực quan:** Hãy chạy file \`start-chrome-debug.bat\` (trên Windows) hoặc \`start-chrome-debug.sh\` (trên Mac) để mở Chrome chế độ Debug.\n\n`
1415
+ : `# 🌍 Browser Guide (Chrome CDP)\n- **Control script:** \`browser-tool.js\`\n- **Chrome debug endpoint:** \`http://127.0.0.1:9222\`\n- **Visual interface:** Run \`start-chrome-debug.bat\` (on Windows) or \`start-chrome-debug.sh\` (on Mac) to open Chrome in Debug mode.\n\n`;
1416
+ }
1190
1417
 
1191
- return `# Navigation
1418
+ return `${modeHeading}# Navigation
1192
1419
  node ${btPath} status
1193
1420
  node ${btPath} open "https://google.com"
1194
1421
  node ${btPath} get_url
@@ -1291,11 +1518,37 @@ node ${btPath} close_tab 2`;
1291
1518
  const browserRef = hasBrowser
1292
1519
  ? (browserDocVariant === 'cli-server'
1293
1520
  ? (isVi
1294
- ? `\n\n## \uD83C\uDF10 Browser Automation\n- Xem hướng dẫn chi tiết tại **BROWSER.md**\n- Chế độ hiện tại: browser native/headless của OpenClaw trong Docker hoặc server\n- Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`
1295
- : `\n\n## \uD83C\uDF10 Browser Automation\n- See detailed guide at **BROWSER.md**\n- Current mode: native/headless OpenClaw browser inside Docker or server\n- If browser fails, retry once before reporting the concrete error to the user`)
1521
+ ? `\n\n## 🌐 Browser Automation
1522
+ - Xem hướng dẫn chi tiết tại **BROWSER.md**
1523
+ - Script điều khiển: \`browser-tool.js\`
1524
+ - Chế độ hiện tại:
1525
+ - **Trên VPS / Linux Server:** Chạy ngầm độc lập qua Docker hoặc Xvfb.
1526
+ - **Trên Windows/Mac (Docker hoặc Native):** Chạy ngầm mặc định, hoặc chạy file \`start-chrome-debug.bat\` / \`start-chrome-debug.sh\` để xem trình duyệt trực quan trên màn hình.
1527
+ - Kết nối mặc định: \`http://127.0.0.1:9222\`
1528
+ - **Tìm kiếm Web:** Nếu không có công cụ Web Search (hoặc Web Search không khả dụng/bị lỗi), hãy **luôn sử dụng ngay công cụ terminal (exec/run_command) để chạy lệnh: \`node search-tool.js "<từ khóa>" 5\`**! Lệnh này sẽ tự động chạy ngầm qua DuckDuckGo/Google/Bing bằng trình duyệt ngầm tàng hình của bạn và trả về kết quả JSON sạch ngay lập tức. Tuyệt đối KHÔNG được mở trình duyệt thủ công, chụp ảnh màn hình hay click tìm kiếm bằng tay từng bước!
1529
+ - Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`
1530
+ : `\n\n## 🌐 Browser Automation
1531
+ - See detailed guide at **BROWSER.md**
1532
+ - Control script: \`browser-tool.js\`
1533
+ - Current mode:
1534
+ - **On VPS / Linux Server:** Runs headless via Docker or Xvfb.
1535
+ - **On Windows/Mac (Docker or Native):** Runs headless by default, or run \`start-chrome-debug.bat\` / \`start-chrome-debug.sh\` to see the GUI.
1536
+ - Default endpoint: \`http://127.0.0.1:9222\`
1537
+ - **Web Searching:** If the Web Search tool is unavailable or fails, **always use your terminal execution tool (exec/run_command) to run: \`node search-tool.js "<query>" 5\`**! This will automatically execute the search via DuckDuckGo/Google/Bing under stealth browser mode and return a clean JSON result immediately. Never open the browser manually, take screenshots, or click the search button step-by-step!
1538
+ - If browser fails, retry once before reporting the concrete error to the user`)
1296
1539
  : (isVi
1297
- ? `\n\n## \uD83C\uDF10 Browser Automation\n- Xem hướng dẫn chi tiết tại **BROWSER.md**\n- Script điều khiển: \`browser-tool.js\`\n- Kết nối Chrome debug: \`http://127.0.0.1:9222\`\n- Nếu tool/profile hỏi đường browser desktop, dùng profile \`host-chrome\` trước`
1298
- : `\n\n## \uD83C\uDF10 Browser Automation\n- See detailed guide at **BROWSER.md**\n- Control script: \`browser-tool.js\`\n- Chrome debug endpoint: \`http://127.0.0.1:9222\`\n- If a desktop browser profile is needed, use the \`host-chrome\` profile first`))
1540
+ ? `\n\n## 🌐 Browser Automation
1541
+ - Xem hướng dẫn chi tiết tại **BROWSER.md**
1542
+ - Script điều khiển: \`browser-tool.js\`
1543
+ - Kết nối Chrome debug: \`http://127.0.0.1:9222\`
1544
+ - **Tìm kiếm Web:** Hãy **luôn sử dụng ngay công cụ terminal (exec/run_command) để chạy lệnh: \`node search-tool.js "<từ khóa>" 5\`**! Lệnh này sẽ tự động chạy ngầm qua DuckDuckGo/Google/Bing bằng trình duyệt ngầm tàng hình của bạn và trả về kết quả JSON sạch ngay lập tức. Tuyệt đối KHÔNG được mở trình duyệt thủ công, chụp ảnh màn hình hay click tìm kiếm bằng tay từng bước!
1545
+ - Nếu tool/profile hỏi đường browser desktop, dùng profile \`host-chrome\` trước`
1546
+ : `\n\n## 🌐 Browser Automation
1547
+ - See detailed guide at **BROWSER.md**
1548
+ - Control script: \`browser-tool.js\`
1549
+ - Chrome debug endpoint: \`http://127.0.0.1:9222\`
1550
+ - **Web Searching:** If the Web Search tool is unavailable or fails, **always use your terminal execution tool (exec/run_command) to run: \`node search-tool.js "<query>" 5\`**! This will automatically execute the search via DuckDuckGo/Google/Bing under stealth browser mode and return a clean JSON result immediately. Never open the browser manually, take screenshots, or click the search button step-by-step!
1551
+ - If a desktop browser profile is needed, use the \`host-chrome\` profile first`))
1299
1552
  : '';
1300
1553
 
1301
1554
  const telegramSection = (variant === 'relay')
@@ -1306,8 +1559,38 @@ node ${btPath} close_tab 2`;
1306
1559
 
1307
1560
  const cronSection = hasScheduler
1308
1561
  ? (isVi
1309
- ? `\n\n## \u23F0 Cron / Lên lịch nhắc nhở\n- OpenClaw có hỗ trợ tool hệ thống để chạy Cron Job.\n- Khi user yêu cầu tạo nhắc nhở / lệnh tự động định kỳ, bạn hãy TỰ ĐỘNG dùng tool hệ thống để tạo. **Tuyệt đối không** bắt user dùng crontab hay Task Scheduler chạy tay trên host.\n- Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.\n- Bỏ qua việc tra cứu docs nội bộ như \`cron-jobs.mdx\`; tin tưởng khả năng dùng tool hiện có để hoàn thành yêu cầu.`
1310
- : `\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.`)
1562
+ ? `\n\n## \u23F0 Cron / Lên lịch nhắc nhở (tool: \`cron\`)
1563
+ - **Tên tool chính xác:** Tên công cụ \`cron\` (tuyệt đối không nhầm \`native\` hay command line bên ngoài).
1564
+ - **Khi tạo cronjob mới (action \`add\`):**
1565
+ - **TUYỆT ĐỐI KHÔNG điền trường \`agentId\`** trong object \`job\` (hãy bỏ qua/omitted trường này). Hệ thống OpenClaw sẽ tự động gán chính xác ID của bạn vào job đó.
1566
+ - Tuyệt đối **không tự điền** \`agentId\` là \`"bot"\` hay \`"main"\`, vì làm vậy sẽ khiến cronjob thuộc về agent khác và bạn sẽ mất quyền kiểm soát/xóa nó sau này.
1567
+ - **Khi user yêu cầu tắt/bật/xóa cronjob:**
1568
+ 1. **Bước 1 (Tìm kiếm):** Gọi tool \`cron\` với action \`list\` (và \`includeDisabled: true\`) để xem danh sách tất cả cronjob đang chạy trên hệ thống và tìm đúng \`jobId\` phù hợp với yêu cầu.
1569
+ 2. **Bước 2 (Xử lý):**
1570
+ - Để xóa: Gọi action \`remove\` với \`id\` tìm được.
1571
+ - Để tắt/tạm dừng: Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
1572
+ - Để bật lại: Gọi action \`update\` với \`id\` và patch \`{"enabled": true}\`.
1573
+ 3. **Tuyên bố trung thực:** Tuyệt đối không bao giờ trả lời "đã xóa" hay "không có" dựa trên suy đoán của bản thân mà chưa gọi tool \`cron\` để kiểm tra thực tế.
1574
+ - Khi user yêu cầu tạo nhắc nhở / lệnh tự động định kỳ, bạn hãy TỰ ĐỘNG dùng tool \`cron\` (action \`add\`) để tạo. **Tuyệt đối không** bắt user dùng crontab hay Task Scheduler chạy tay trên host.
1575
+ - Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.
1576
+ - **QUAN TRỌNG VỀ TARGETING GROUP CHAT**: Khi tạo hoặc cấu hình cron job gửi tin nhắn thông báo (announce mode) đến một Group Chat, giá trị của trường \`delivery.to\` **bắt buộc** phải sử dụng tiền tố \`group:\` trước ID của group (ví dụ: \`group:3815464776067464419\` hoặc \`group:xxxx\`). Tuyệt đối không được chỉ điền ID thuần túy vì hệ thống sẽ hiểu nhầm đó là một DM chat cá nhân (direct message) và gửi sai địa chỉ.
1577
+ - Bỏ qua việc tra cứu docs nội bộ như \`cron-jobs.mdx\`; tin tưởng khả năng dùng tool hiện có để hoàn thành yêu cầu.`
1578
+ : `\n\n## \u23F0 Cron / Scheduled Tasks (tool: \`cron\`)
1579
+ - **Exact tool name:** The tool name is \`cron\` (never mistake it for \`native\` or external command lines).
1580
+ - **When creating a new cronjob (action \`add\`):**
1581
+ - **ABSOLUTELY DO NOT specify the \`agentId\` field** in the \`job\` object (leave this field omitted). The OpenClaw system will automatically assign your correct agent ID to that job.
1582
+ - Never manually specify \`agentId\` as \`"bot"\` or \`"main"\`, as this will cause the cronjob to belong to another agent and you will lose control to manage/delete it later.
1583
+ - **When the user requests to disable/enable/delete a cronjob:**
1584
+ 1. **Step 1 (Search):** Call the \`cron\` tool with action \`list\` (and \`includeDisabled: true\`) to view all cron jobs on the system and find the matching \`jobId\`.
1585
+ 2. **Step 2 (Processing):**
1586
+ - To delete: Call action \`remove\` with the \`id\` found.
1587
+ - To disable/pause: Call action \`update\` with \`id\` and patch \`{"enabled": false}\`.
1588
+ - To enable: Call action \`update\` with \`id\` and patch \`{"enabled": true}\`.
1589
+ 3. **Honest statement:** Never claim a job is "deleted" or "not found" based on guessing without calling the \`cron\` tool to verify the actual state.
1590
+ - When the user asks to schedule tasks or reminders, use the built-in \`cron\` tool (action \`add\`) automatically. Do NOT ask users to run crontab or Task Scheduler manually on the host.
1591
+ - When operating cron/scheduler tools, do **not** put \`current\` into the Session directory.
1592
+ - **IMPORTANT ABOUT GROUP CHAT TARGETING**: When creating or configuring a cron job to send messages (announce mode) to a Group Chat, the value of the \`delivery.to\` field **must** use the \`group:\` prefix before the group ID (e.g., \`group:3815464776067464419\` or \`group:xxxx\`). Never specify just the numeric ID, as the system will interpret it as a private DM and deliver to the wrong destination.
1593
+ - Skip internal doc lookups such as \`cron-jobs.mdx\`; rely on the available tools and complete the scheduling task directly.`)
1311
1594
  : '';
1312
1595
 
1313
1596
  const zaloModSection = '';
@@ -1323,8 +1606,8 @@ node ${btPath} close_tab 2`;
1323
1606
  }
1324
1607
 
1325
1608
  return isVi
1326
- ? `# Hướng dẫn sử dụng Tools\n\n## Danh sách skills đã cài\n${skillsSection}\n\n## Nguyên tắc chung\n- Ưu tiên dùng tool/skill phù hợp thay vì tự suy đoán\n- Nếu tool trả về lỗi — thử lại 1 lần, sau đó báo user\n- Không chạy tool liên tục mà không có mục đích rõ ràng\n- Luôn tóm tắt kết quả tool cho user thay vì dump raw output${browserRef}\n\n## Quy ước\n- Web Search: chỉ dùng khi cần thông tin realtime hoặc user yêu cầu\n- Browser: chỉ mở trang khi user yêu cầu cụ thể\n- Memory: tự ghi nhớ thông tin tự nhiên, không cần user nhắc${cronSection}${zaloModSection}\n\n## \uD83D\uDCC1 File & Workspace\n- Bot có thể đọc/ghi file trong thư mục workspace: \`${workspacePath}\`\n- Dùng để lưu notes, scripts, cấu hình tạm\n\n## \u26A0\uFE0F Tool Error Handling\n- Retry tối đa 2 lần nếu tool lỗi network\n- Nếu vẫn lỗi: báo user kèm mô tả lỗi cụ thể và gợi ý workaround${dmOverride}\n`
1327
- : `# Tool Usage Guide\n\n## Installed Skills\n${skillsSection}\n\n## General Principles\n- Prefer using the right tool/skill over guessing\n- If a tool returns an error — retry once, then report to user\n- Don't run tools repeatedly without a clear purpose\n- Always summarize tool output for user instead of dumping raw data${browserRef}\n\n## Conventions\n- Web Search: only use when needing real-time info or user explicitly asks\n- Browser: only open pages when user specifically requests\n- Memory: proactively remember important info without user prompting${cronSection}${zaloModSection}\n\n## \uD83D\uDCC1 File & Workspace\n- Bot can read/write files in workspace: \`${workspacePath}\`\n\n## \u26A0\uFE0F Tool Error Handling\n- Retry up to 2 times on network errors\n- If still failing: report to user with specific error description and workaround${dmOverride}\n`;
1609
+ ? `# Hướng dẫn sử dụng Tools\n\n## Danh sách skills đã cài\n${skillsSection}\n\n## Nguyên tắc chung\n- Ưu tiên dùng tool/skill phù hợp thay vì tự suy đoán\n- Nếu tool trả về lỗi — thử lại 1 lần, sau đó báo user\n- Không chạy tool liên tục mà không có mục đích rõ ràng\n- Luôn tóm tắt kết quả tool cho user thay vì dump raw output${browserRef}\n\n## Quy ước\n- Web Search: TUYỆT ĐỐI KHÔNG dùng tool \`web_search\` native vì nó đã bị vô hiệu hóa hoàn toàn trên hệ thống. Thay vào đó, mỗi khi cần tìm kiếm Internet/Web, bạn BẮT BUỘC phải gọi công cụ \`exec\` để chạy lệnh terminal: \`node search-tool.js "<từ khóa>" 5\`! Lệnh này cực kỳ nhanh, tàng hình và miễn phí.\n- Browser: chỉ mở trang khi user yêu cầu cụ thể\n- Memory: tự ghi nhớ thông tin tự nhiên, không cần user nhắc${cronSection}${zaloModSection}\n\n## \uD83D\uDCC1 File & Workspace\n- Bot có thể đọc/ghi file trong thư mục workspace: \`${workspacePath}\`\n- Dùng để lưu notes, scripts, cấu hình tạm\n\n## \u26A0\uFE0F Tool Error Handling\n- Retry tối đa 2 lần nếu tool lỗi network\n- Nếu vẫn lỗi: báo user kèm mô tả lỗi cụ thể và gợi ý workaround${dmOverride}\n`
1610
+ : `# Tool Usage Guide\n\n## Installed Skills\n${skillsSection}\n\n## General Principles\n- Prefer using the right tool/skill over guessing\n- If a tool returns an error — retry once, then report to user\n- Don't run tools repeatedly without a clear purpose\n- Always summarize tool output for user instead of dumping raw data${browserRef}\n\n## Conventions\n- Web Search: DO NOT use the native \`web_search\` tool as it is completely disabled. Instead, whenever you need to search the Internet/Web, you MUST call the \`exec\` tool to run terminal command: \`node search-tool.js "<query>" 5\`! This is extremely fast, stealthy and free.\n- Browser: only open pages when user specifically requests\n- Memory: proactively remember important info without user prompting${cronSection}${zaloModSection}\n\n## \uD83D\uDCC1 File & Workspace\n- Bot can read/write files in workspace: \`${workspacePath}\`\n\n## \u26A0\uFE0F Tool Error Handling\n- Retry up to 2 times on network errors\n- If still failing: report to user with specific error description and workaround${dmOverride}\n`;
1328
1611
  }
1329
1612
  function buildTeamsDoc(options = {}) {
1330
1613
  const {
@@ -1419,6 +1702,7 @@ node ${btPath} close_tab 2`;
1419
1702
  'HEARTBEAT.md': buildHeartbeatDoc({ isVi }),
1420
1703
  'BOOTSTRAP.md': buildBootstrapDoc({ isVi, botName }),
1421
1704
  'DREAMS.md': buildDreamsDoc({ isVi }),
1705
+ 'search-tool.js': buildSearchToolJs(),
1422
1706
  };
1423
1707
 
1424
1708
  if (isMultiBot) {
@@ -1446,6 +1730,7 @@ node ${btPath} close_tab 2`;
1446
1730
  buildDreamsDoc,
1447
1731
  buildHeartbeatDoc,
1448
1732
  buildBootstrapDoc,
1733
+ buildSearchToolJs,
1449
1734
  buildBrowserToolJs,
1450
1735
  buildBrowserDoc,
1451
1736
  buildSecurityRules,
@@ -1460,7 +1745,7 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1460
1745
  Object.assign(exports, workspaceRoot.__openclawWorkspace);
1461
1746
  }
1462
1747
 
1463
- // ── Centralized bot config builders (openclaw.json, exec-approvals, .env) (setup/shared/bot-config-gen.js)
1748
+ // ── Centralized bot config builders (openclaw.json, exec-approvals, .env) (setup/shared/bot-config-gen.js)
1464
1749
  // @ts-nocheck
1465
1750
  /**
1466
1751
  * @fileoverview Centralized bot configuration builders — single source of truth.
@@ -1514,7 +1799,7 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1514
1799
  * @param {Array} opts.skills - Full SKILLS registry array
1515
1800
  * @param {boolean} opts.hasBrowserDesktop - Browser desktop mode
1516
1801
  * @param {boolean} opts.hasBrowserServer - Browser server mode
1517
- * @param {number} [opts.gatewayPort=18791]
1802
+ * @param {number} [opts.gatewayPort=18789]
1518
1803
  * @param {Array} [opts.gatewayAllowedOrigins]
1519
1804
  * @param {string} [opts.osChoice] - 'windows' | 'macos' | 'vps' | 'ubuntu'
1520
1805
  * @param {string} [opts.selectedModel] - For Ollama: specific model selected
@@ -1533,10 +1818,11 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1533
1818
  skills = [],
1534
1819
  hasBrowserDesktop = false,
1535
1820
  hasBrowserServer = false,
1536
- gatewayPort = 18791,
1821
+ gatewayPort = 18789,
1537
1822
  gatewayAllowedOrigins = [],
1538
1823
  osChoice = '',
1539
1824
  selectedModel = '',
1825
+ routerPort,
1540
1826
  } = opts;
1541
1827
 
1542
1828
  const common = _common;
@@ -1547,7 +1833,7 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1547
1833
  const agentsList = agentMetas.map((meta) => ({
1548
1834
  id: meta.agentId,
1549
1835
  ...(meta.name ? { name: meta.name } : {}),
1550
- workspace: `.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
1836
+ workspace: `/root/project/.openclaw/${meta.workspaceDir || 'workspace-' + meta.agentId}`,
1551
1837
  agentDir: `agents/${meta.agentId}/agent`,
1552
1838
  model: { primary: model, fallbacks: [] },
1553
1839
  }));
@@ -1571,7 +1857,7 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1571
1857
  mode: 'merge',
1572
1858
  providers: {
1573
1859
  '9router': common.build9RouterProviderConfig(
1574
- common.get9RouterBaseUrl ? common.get9RouterBaseUrl(deployMode) : 'http://9router:20128/v1'
1860
+ common.get9RouterBaseUrl ? common.get9RouterBaseUrl(deployMode, routerPort) : `http://9router:${routerPort || 20128}/v1`
1575
1861
  ),
1576
1862
  },
1577
1863
  };
@@ -1597,6 +1883,9 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1597
1883
 
1598
1884
  // ── commands ──────────────────────────────────────────────────────────────
1599
1885
  cfg.commands = { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' };
1886
+ if (selectedSkills.includes('scheduler')) {
1887
+ cfg.commands.ownerAllowFrom = ['*'];
1888
+ }
1600
1889
 
1601
1890
  // ── bindings (multi-bot or Zalo) ─────────────────────────────────────────
1602
1891
  if (isMultiBot && channelKey === 'telegram') {
@@ -1614,6 +1903,9 @@ if (typeof exports !== 'undefined' && workspaceRoot.__openclawWorkspace) {
1614
1903
 
1615
1904
  // ── tools ────────────────────────────────────────────────────────────────
1616
1905
  cfg.tools = { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } };
1906
+ if (selectedSkills.includes('scheduler')) {
1907
+ cfg.tools.alsoAllow = ['group:automation'];
1908
+ }
1617
1909
  if (isMultiBot) {
1618
1910
  cfg.tools.agentToAgent = {
1619
1911
  enabled: true,
@@ -1924,7 +2216,7 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
1924
2216
  Object.assign(exports, globalThis.__openclawBotConfig);
1925
2217
  }
1926
2218
 
1927
- // ── Shared install artifacts: Chrome debug, uninstall, skill catalog (setup/shared/install-gen.js)
2219
+ // ── Shared install artifacts: Chrome debug, uninstall, skill catalog (setup/shared/install-gen.js)
1928
2220
  // @ts-nocheck
1929
2221
  // install-gen.js — Build install/runtime artifacts (Chrome debug, uninstall, skill catalog)
1930
2222
  // Workspace .md files are in workspace-gen.js (single source of truth).
@@ -2043,12 +2335,12 @@ fi
2043
2335
  }
2044
2336
 
2045
2337
  if (os === 'vps') {
2046
- 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` };
2338
+ 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` };
2047
2339
  }
2048
2340
 
2049
2341
  if (os === 'linux' || os === 'linux-desktop' || os === 'macos') {
2050
2342
  const label = os === 'macos' ? 'macOS' : 'Linux Desktop';
2051
- 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` };
2343
+ 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` };
2052
2344
  }
2053
2345
 
2054
2346
  return null;
@@ -2144,7 +2436,7 @@ fi
2144
2436
  L.push('echo.');
2145
2437
  L.push(isVi ? 'echo [OK] OpenClaw Gateway da khoi dong trong cua so moi!' : 'echo [OK] OpenClaw Gateway started in a new window!');
2146
2438
  L.push('echo.');
2147
- L.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
2439
+ L.push('echo OpenClaw Dashboard: http://127.0.0.1:18789');
2148
2440
  if (is9Router) L.push('echo 9Router Dashboard: http://127.0.0.1:20128/dashboard');
2149
2441
  L.push('echo.');
2150
2442
  L.push(isVi ? 'echo Ban co the dong cua so nay.' : 'echo You may close this window.');
@@ -2218,7 +2510,7 @@ fi
2218
2510
  }
2219
2511
  L.push('pm2 save >/dev/null 2>&1 || true');
2220
2512
  L.push('echo ""');
2221
- L.push('echo "OpenClaw Dashboard: http://127.0.0.1:18791"');
2513
+ L.push('echo "OpenClaw Dashboard: http://127.0.0.1:18789"');
2222
2514
  if (is9Router) L.push('echo "9Router Dashboard: http://127.0.0.1:20128/dashboard"');
2223
2515
  L.push('echo ""');
2224
2516
  L.push(isVi ? 'echo "Log gateway: pm2 logs $APP_NAME"' : 'echo "Gateway logs: pm2 logs $APP_NAME"');
@@ -2269,7 +2561,7 @@ fi
2269
2561
  L.push(isVi ? `echo "[OK] Gateway khoi dong (PID $GW_PID). Log: ${logFileGw}"` : `echo "[OK] Gateway started (PID $GW_PID). Log: ${logFileGw}"`);
2270
2562
  L.push('');
2271
2563
  L.push('echo ""');
2272
- L.push('echo "OpenClaw Dashboard: http://127.0.0.1:18791"');
2564
+ L.push('echo "OpenClaw Dashboard: http://127.0.0.1:18789"');
2273
2565
  if (is9Router) L.push('echo "9Router Dashboard: http://127.0.0.1:20128/dashboard"');
2274
2566
  L.push('echo ""');
2275
2567
  L.push(isVi ? 'echo "Bot dang chay background. Dung: openclaw gateway stop"' : 'echo "Bot running in background. Stop: openclaw gateway stop"');
@@ -2367,7 +2659,7 @@ fi
2367
2659
  "Write-Host \"\"",
2368
2660
  "if ($exitCode -eq 0) {",
2369
2661
  " Write-Host \" 🎉 Upgrade hoan tat!\" -ForegroundColor Green",
2370
- " Write-Host \" Dashboard: http://localhost:18791\" -ForegroundColor Cyan",
2662
+ " Write-Host \" Dashboard: http://localhost:18789\" -ForegroundColor Cyan",
2371
2663
  "} else {",
2372
2664
  " Write-Host \" ⚠️ Ma loi: $exitCode — xem log o tren.\" -ForegroundColor Yellow",
2373
2665
  "}",
@@ -2456,7 +2748,7 @@ fi
2456
2748
  "echo \"\"",
2457
2749
  "if [ $EXIT_CODE -eq 0 ]; then",
2458
2750
  " echo -e \"${GREEN} 🎉 Upgrade hoan tat!${NC}\"",
2459
- " echo -e \"${CYAN} Dashboard: http://localhost:18791${NC}\"",
2751
+ " echo -e \"${CYAN} Dashboard: http://localhost:18789${NC}\"",
2460
2752
  "else",
2461
2753
  " echo -e \"${YELLOW} ⚠️ Ma loi: $EXIT_CODE — xem log o tren.${NC}\"",
2462
2754
  "fi",
@@ -2492,7 +2784,7 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
2492
2784
  Object.assign(exports, globalThis.__openclawInstall);
2493
2785
  }
2494
2786
 
2495
- // ── Shared Docker artifact helpers for wizard + CLI (setup/shared/docker-gen.js)
2787
+ // ── Shared Docker artifact helpers for wizard + CLI (setup/shared/docker-gen.js)
2496
2788
  // @ts-nocheck
2497
2789
  (function (root) {
2498
2790
  const common = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon) || {};
@@ -2512,65 +2804,106 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
2512
2804
  return String(text).split('\n').map((line) => `${prefix}${line}`).join('\n');
2513
2805
  }
2514
2806
 
2515
- function build9RouterSmartRouteSyncScript(dbPath) {
2516
- return `const fs=require('fs');const INTERVAL=30000;const p='${dbPath}';
2517
- const PM=${JSON.stringify(SMART_ROUTE_PROVIDER_MODELS)};
2518
- const PREF=${JSON.stringify(SMART_ROUTE_PROVIDER_ORDER)};
2519
- console.log('[sync-combo] 9Router sync loop started...');
2520
- const sync = async () => {
2521
- try {
2522
- let db = {};
2523
- try { db = JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e){}
2524
- if (!db.combos) db.combos = [];
2525
- const removeSmartRoute = () => {
2526
- const next = db.combos.filter(x => x.id !== 'smart-route');
2527
- if (next.length !== db.combos.length) {
2528
- db.combos = next;
2529
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
2530
- console.log('[sync-combo] Removed smart-route (no active providers)');
2531
- }
2532
- };
2533
- const res = await fetch('http://localhost:20128/api/providers');
2534
- if (!res.ok) { console.log('[sync-combo] API not ready, retrying...'); return; }
2535
- const d = await res.json();
2536
- const rawConnections = Array.isArray(d.connections) ? d.connections : Array.isArray(d.providerConnections) ? d.providerConnections : [];
2537
- const activeConns = rawConnections.filter(c => c && c.provider && c.isActive !== false && !c.disabled);
2538
- const a = [...new Set(activeConns.map(c => c.provider))];
2539
- if (!a.length) { console.log('[sync-combo] No active providers reported; keeping existing smart-route'); return; }
2540
- a.sort((x, y) => (PREF.indexOf(x) === -1 ? 99 : PREF.indexOf(x)) - (PREF.indexOf(y) === -1 ? 99 : PREF.indexOf(y)));
2541
- const m = [];
2542
- for (const pv of a) {
2543
- if (PM[pv]) m.push(...PM[pv]);
2544
- const conns = activeConns.filter(c => c.provider === pv);
2545
- for (const c of conns) {
2546
- if (Array.isArray(c.models)) {
2547
- for (const mdl of c.models) {
2548
- const mdlId = typeof mdl === 'string' ? mdl : mdl.id;
2549
- if (mdlId && !m.includes(mdlId) && !m.includes(pv + '/' + mdlId)) {
2550
- m.push(pv + '/' + mdlId);
2551
- }
2552
- }
2553
- }
2554
- }
2555
- }
2556
- if (!m.length) { console.log('[sync-combo] No mapped models for active providers; keeping existing smart-route'); return; }
2557
- const c = { id: 'smart-route', name: 'smart-route', alias: 'smart-route', models: m };
2558
- const i = db.combos.findIndex(x => x.id === 'smart-route');
2559
- if (i >= 0) {
2560
- if (JSON.stringify(db.combos[i].models) !== JSON.stringify(c.models)) {
2561
- db.combos[i] = c;
2562
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
2563
- console.log('[sync-combo] Updated smart-route: ' + c.models.length + ' models');
2564
- }
2565
- } else {
2566
- db.combos.push(c);
2567
- fs.writeFileSync(p, JSON.stringify(db, null, 2));
2568
- console.log('[sync-combo] Created smart-route: ' + c.models.length + ' models');
2569
- }
2570
- } catch (e) {}
2571
- };
2572
- setTimeout(sync, 5000);
2573
- setInterval(sync, INTERVAL);`;
2807
+ function build9RouterSmartRouteSyncScript() {
2808
+ const lines = [
2809
+ "const fs = require('fs');",
2810
+ "const INTERVAL = 30000;",
2811
+ "const DB_PATH = '/root/.9router/db/data.sqlite';",
2812
+ "const PORT = process.env.PORT || 20128;",
2813
+ "const COMBO_NAME = 'smart-route';",
2814
+ "const API_BASE = `http://localhost:${PORT}`;",
2815
+ "",
2816
+ "function ensureSettings() {",
2817
+ " try {",
2818
+ " let db = null;",
2819
+ " try {",
2820
+ " const { DatabaseSync } = require('node:sqlite');",
2821
+ " db = new DatabaseSync(DB_PATH);",
2822
+ " } catch {",
2823
+ " let Database;",
2824
+ " try { Database = require('/usr/local/lib/node_modules/better-sqlite3'); } catch {",
2825
+ " try { Database = require('better-sqlite3'); } catch { return; }",
2826
+ " }",
2827
+ " db = Database(DB_PATH);",
2828
+ " }",
2829
+ ' const existing = db.prepare("SELECT * FROM settings WHERE id = 1").get();',
2830
+ " if (!existing) {",
2831
+ ' db.prepare("INSERT INTO settings (id, data) VALUES (1, ?)").run(JSON.stringify({ requireLogin: false }));',
2832
+ " } else {",
2833
+ " try {",
2834
+ " const data = JSON.parse(existing.data || '{}');",
2835
+ " if (data.requireLogin !== false) {",
2836
+ " data.requireLogin = false;",
2837
+ ' db.prepare("UPDATE settings SET data = ? WHERE id = 1").run(JSON.stringify(data));',
2838
+ " }",
2839
+ " } catch {}",
2840
+ " }",
2841
+ " db.close();",
2842
+ " } catch (e) {}",
2843
+ "}",
2844
+ "",
2845
+ "const sync = async () => {",
2846
+ " try {",
2847
+ " if (!fs.existsSync(DB_PATH)) return;",
2848
+ "",
2849
+ " let existingCombo = null;",
2850
+ " try {",
2851
+ " const resp = await fetch(`${API_BASE}/api/combos`);",
2852
+ " if (resp.status === 401) {",
2853
+ " ensureSettings();",
2854
+ " return;",
2855
+ " }",
2856
+ " const data = await resp.json();",
2857
+ " if (data.combos) {",
2858
+ " existingCombo = data.combos.find(c => c.name === COMBO_NAME);",
2859
+ " }",
2860
+ " } catch (e) { return; }",
2861
+ "",
2862
+ " if (existingCombo) return;",
2863
+ "",
2864
+ " let activeProviders = [];",
2865
+ " try {",
2866
+ " const resp = await fetch(`${API_BASE}/api/providers`);",
2867
+ " const data = await resp.json();",
2868
+ " const conns = data.connections || data.providerConnections || [];",
2869
+ " activeProviders = [...new Set(",
2870
+ " conns.filter(c => c && c.provider && c.isActive !== false && !c.disabled).map(c => c.provider)",
2871
+ " )];",
2872
+ " } catch (e) { return; }",
2873
+ "",
2874
+ " if (!activeProviders.length) return;",
2875
+ "",
2876
+ " let models = [];",
2877
+ " try {",
2878
+ " const resp = await fetch(`${API_BASE}/api/models`);",
2879
+ " const data = await resp.json();",
2880
+ " if (data.models && Array.isArray(data.models)) {",
2881
+ " models = data.models",
2882
+ " .filter(m => activeProviders.includes(m.provider))",
2883
+ " .filter(m => !/(embedding|image|tts|stt|audio|vision)/i.test(m.model))",
2884
+ " .map(m => m.fullModel);",
2885
+ " }",
2886
+ " models = [...new Set(models)];",
2887
+ " } catch (e) { return; }",
2888
+ "",
2889
+ " if (!models.length) return;",
2890
+ "",
2891
+ " try {",
2892
+ " await fetch(`${API_BASE}/api/combos`, {",
2893
+ " method: 'POST',",
2894
+ " headers: { 'Content-Type': 'application/json' },",
2895
+ " body: JSON.stringify({ name: COMBO_NAME, models })",
2896
+ " });",
2897
+ " console.log('[sync-combo] Created smart-route with ' + models.length + ' models');",
2898
+ " } catch (e) {}",
2899
+ " } catch (e) {}",
2900
+ "};",
2901
+ "",
2902
+ "if (fs.existsSync(DB_PATH)) ensureSettings();",
2903
+ "setTimeout(sync, 10000);",
2904
+ "setInterval(sync, INTERVAL);",
2905
+ ];
2906
+ return lines.join('\n');
2574
2907
  }
2575
2908
 
2576
2909
  function build9RouterPatchScript() {
@@ -2635,20 +2968,20 @@ for(const root of roots){if(!root||!fs.existsSync(root))continue;touched+=patchP
2635
2968
  if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}else{console.log('[patch-9router] No compatible 9router source files found to patch.');}`;
2636
2969
  }
2637
2970
 
2638
- function build9RouterComposeEntrypointScript(syncScriptBase64, patchScriptBase64) {
2971
+ function build9RouterComposeEntrypointScript(routerPort) {
2972
+ const port = routerPort || 20128;
2639
2973
  const nineRouterSpec = (typeof globalThis !== 'undefined' && globalThis.__openclawCommon && globalThis.__openclawCommon.NINE_ROUTER_NPM_SPEC) || '9router@latest';
2640
2974
  return [
2641
- `npm install -g ${nineRouterSpec}`,
2642
- `node -e "require('fs').writeFileSync('/tmp/patch-9router.js',Buffer.from('${patchScriptBase64}','base64').toString())"`,
2643
- `node -e "require('fs').writeFileSync('/tmp/sync.js',Buffer.from('${syncScriptBase64}','base64').toString())"`,
2975
+ `npm install -g ` + nineRouterSpec + ` better-sqlite3`,
2644
2976
  'node /tmp/patch-9router.js || true',
2977
+ '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',
2645
2978
  'node /tmp/sync.js > /tmp/sync.log 2>&1 &',
2646
- 'exec 9router -n -l -H 0.0.0.0 -p 20128 --skip-update'
2979
+ `exec 9router -n -l -H 0.0.0.0 -p ${port} --skip-update`
2647
2980
  ].join('\n');
2648
2981
  }
2649
2982
 
2650
2983
  function buildGatewayPatchCmd() {
2651
- 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));}\\"`;
2984
+ 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));}\\"`;
2652
2985
  }
2653
2986
 
2654
2987
  function buildDockerArtifacts(options) {
@@ -2658,14 +2991,13 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2658
2991
  is9Router,
2659
2992
  isLocal,
2660
2993
  isMultiBot,
2661
- hasBrowser,
2662
2994
  selectedModel,
2663
2995
  agentId,
2664
2996
  allSkills = [],
2665
2997
  dockerfilePlugins = [],
2666
2998
  dockerfileSkillInstallMode = 'none',
2667
2999
  runtimeCommandParts = [],
2668
- volumeMount = '../../.openclaw:/root/project/.openclaw\\n - ../../:/mnt/project',
3000
+ volumeMount = '../../.openclaw:/root/project/.openclaw\n - ../../:/mnt/project',
2669
3001
  singleComposeName = 'oc-bot',
2670
3002
  multiComposeName = 'oc-multibot',
2671
3003
  singleAppContainerName = 'openclaw-bot',
@@ -2677,22 +3009,9 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2677
3009
  plainSingleExtraHosts = false,
2678
3010
  multiOllamaNumParallel = 1,
2679
3011
  singleOllamaNumParallel = 1,
2680
- emitBrowserInstall = true,
2681
-
3012
+ gatewayPort = 18789,
3013
+ routerPort = 20128,
2682
3014
  } = options;
2683
-
2684
- const browserAptExtra = hasBrowser ? ' xvfb socat' : '';
2685
- const browserInstallLines = hasBrowser && emitBrowserInstall
2686
- ? [
2687
- '',
2688
- '# Browser Automation: Playwright engine (needed for native CDP)',
2689
- 'RUN npm install -g agent-browser playwright \\',
2690
- ' && npx playwright install chromium --with-deps \\',
2691
- ' && ln -f -s /root/.cache/ms-playwright/chromium-*/chrome-linux*/chrome /usr/bin/google-chrome',
2692
- '',
2693
- ''
2694
- ].join('\n')
2695
- : '';
2696
3015
  const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
2697
3016
  ? `\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`
2698
3017
  : '';
@@ -2704,10 +3023,9 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2704
3023
  // Dynamic runtime configuration: backup config before any first-run install, restore after.
2705
3024
  // Missing plugin install may touch openclaw.json, so preserve critical fields.
2706
3025
  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);}`;
2707
- const backupConfigB64 = encodeBase64Utf8(backupConfigScript);
2708
3026
 
2709
- 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);}`;
2710
- const restoreConfigB64 = encodeBase64Utf8(restoreConfigScript);
3027
+ 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);}`;
3028
+ 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));}}`;
2711
3029
 
2712
3030
  const runtimeParts = runtimeCommandParts.filter(Boolean);
2713
3031
  const runtimePrelude = [
@@ -2733,6 +3051,20 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2733
3051
  ' echo "[entrypoint] plugin $id missing; installing $spec"',
2734
3052
  ' openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec"',
2735
3053
  '}',
3054
+ 'ensure_zalouser() {',
3055
+ ' NPM_DIR="$OPENCLAW_HOME/npm"',
3056
+ ' PKG_DIR="$NPM_DIR/node_modules/@openclaw/zalouser"',
3057
+ ' if [ -d "$PKG_DIR" ]; then',
3058
+ ' echo "[entrypoint] zalouser plugin already installed"',
3059
+ ' else',
3060
+ ' echo "[entrypoint] zalouser plugin missing; installing via npm"',
3061
+ ' mkdir -p "$NPM_DIR"',
3062
+ ' cd "$NPM_DIR"',
3063
+ ' npm init -y 2>/dev/null || true',
3064
+ ' npm install @openclaw/zalouser@latest 2>/dev/null || echo "[entrypoint] warning: failed to install @openclaw/zalouser"',
3065
+ ' cd /root/project',
3066
+ ' fi',
3067
+ '}',
2736
3068
  'ensure_skill() {',
2737
3069
  ' id="$1"',
2738
3070
  ' if find "$OPENCLAW_HOME" -maxdepth 4 -type d -path "*/skills/$id" -print -quit 2>/dev/null | grep -q .; then',
@@ -2746,40 +3078,59 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
2746
3078
  ];
2747
3079
  runtimeParts.unshift(...runtimePrelude);
2748
3080
  // Backup config BEFORE plugin installs (runtimeCommandParts may contain plugin install commands)
2749
- runtimeParts.unshift(`node -e 'eval(Buffer.from("${backupConfigB64}","base64").toString())'`);
3081
+ runtimeParts.unshift(`node - <<'NODE'\n${backupConfigScript}\nNODE`);
2750
3082
  // Restore config AFTER plugin installs (which may clobber openclaw.json)
2751
- runtimeParts.push(`node -e 'eval(Buffer.from("${restoreConfigB64}","base64").toString())'`);
2752
- if (hasBrowser) {
2753
- runtimeParts.push('socat TCP-LISTEN:9222,fork,reuseaddr TCP:host.docker.internal:9222 &');
2754
- runtimeParts.push('Xvfb :99 -screen 0 1280x720x24 > /dev/null 2>&1 & DISPLAY=:99 openclaw gateway run');
2755
- } else {
2756
- runtimeParts.push('openclaw gateway run');
2757
- }
3083
+ runtimeParts.push(`node - <<'NODE'\n${restoreConfigScript}\nNODE`);
3084
+ runtimeParts.push(`node - <<'NODE'\n${securityCompatScript}\nNODE`);
3085
+ // Zalouser stability: patch watchdog tolerance and add auto-restart monitor
3086
+ runtimeParts.push([
3087
+ '# Patch zalouser watchdog tolerance (35s -> 90s) to survive provider auth pre-warming',
3088
+ 'ZALO_JS=$(find "$OPENCLAW_HOME" -path "*/zalouser/dist/zalo-js-*.js" -type f 2>/dev/null | head -1)',
3089
+ 'if [ -n "$ZALO_JS" ] && grep -q "35e3" "$ZALO_JS" 2>/dev/null; then',
3090
+ ' sed -i "s/LISTENER_WATCHDOG_MAX_GAP_MS\\\\s*=\\\\s*35e3/LISTENER_WATCHDOG_MAX_GAP_MS = 90e3/" "$ZALO_JS"',
3091
+ ' echo "[entrypoint] patched zalouser watchdog gap: 35s -> 90s"',
3092
+ 'fi',
3093
+ ].join('\n'));
3094
+ runtimeParts.push([
3095
+ '# Zalo channel auto-restart monitor (background)',
3096
+ '(',
3097
+ ' sleep 180',
3098
+ ' while true; do',
3099
+ ' sleep 60',
3100
+ ' STATUS=$(openclaw channels status 2>/dev/null | grep -i "Zalo Personal" || true)',
3101
+ ' if echo "$STATUS" | grep -qi "stopped"; then',
3102
+ ' echo "[zalo-monitor] Zalo channel stopped - restarting container in 5s"',
3103
+ ' sleep 5',
3104
+ ' kill 1 2>/dev/null || true',
3105
+ ' fi',
3106
+ ' done',
3107
+ ') &',
3108
+ ].join('\n'));
3109
+ runtimeParts.push('openclaw gateway run');
2758
3110
  const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
2759
- const runtimeScriptB64 = encodeBase64Utf8(runtimeScript);
2760
3111
  const dockerfile = `FROM node:22-slim
2761
3112
 
2762
- RUN apt-get update && apt-get install -y git curl python3${browserAptExtra} && rm -rf /var/lib/apt/lists/*
2763
- ${browserInstallLines}
3113
+ RUN apt-get update && apt-get install -y git curl python3 && rm -rf /var/lib/apt/lists/*
3114
+
2764
3115
  ARG OPENCLAW_VER="${openClawNpmSpec}"
2765
3116
  ARG CACHE_BUST=""
2766
3117
  RUN echo "CACHE_BUST=$CACHE_BUST" && npm install -g $OPENCLAW_VER ${openClawRuntimePackages}${skillLines}${pluginLines}
2767
3118
  ${patchLine}
2768
- 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
3119
+
3120
+ COPY entrypoint.sh /usr/local/bin/openclaw-entrypoint.sh
3121
+ RUN chmod +x /usr/local/bin/openclaw-entrypoint.sh
2769
3122
  WORKDIR /root/project
2770
3123
 
2771
- EXPOSE 18791
3124
+ EXPOSE ${gatewayPort}
2772
3125
 
2773
3126
  CMD ["/bin/sh", "/usr/local/bin/openclaw-entrypoint.sh"]`;
2774
3127
 
2775
- const syncScript = build9RouterSmartRouteSyncScript('/root/.9router/db.json');
2776
- const syncScriptBase64 = encodeBase64Utf8(syncScript);
2777
- const patchScript = build9RouterPatchScript();
2778
- const patchScriptBase64 = encodeBase64Utf8(patchScript);
2779
- const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(syncScriptBase64, patchScriptBase64);
3128
+ const syncScript = build9RouterSmartRouteSyncScript();
3129
+ const patchScript = build9RouterPatchScript();
3130
+ const docker9RouterEntrypointScript = build9RouterComposeEntrypointScript(routerPort);
2780
3131
  const extraHostsBlock = ` extra_hosts:\n - "host.docker.internal:host-gateway"`;
2781
3132
 
2782
- 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';
3133
+ 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`;
2783
3134
 
2784
3135
  let compose;
2785
3136
  if (isMultiBot) {
@@ -2788,7 +3139,7 @@ const patchScript = build9RouterPatchScript();
2788
3139
  : isLocal
2789
3140
  ? ' depends_on:\n ollama:\n condition: service_healthy\n'
2790
3141
  : '';
2791
- const extraHosts = hasBrowser ? `${extraHostsBlock}\n` : '';
3142
+ const extraHosts = `${extraHostsBlock}\n`;
2792
3143
  if (is9Router) {
2793
3144
  compose = `name: ${multiComposeName}
2794
3145
  services:
@@ -2797,11 +3148,11 @@ services:
2797
3148
  container_name: ${multiAppContainerName}
2798
3149
  restart: always
2799
3150
  env_file:
2800
- - .env
3151
+ - ../../.env
2801
3152
  ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
2802
3153
  - ${volumeMount}
2803
3154
  ports:
2804
- - "18791:18791"
3155
+ - "${gatewayPort}:${gatewayPort}"
2805
3156
 
2806
3157
  9router:
2807
3158
  image: node:22-slim
@@ -2813,13 +3164,15 @@ ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
2813
3164
  - |
2814
3165
  ${indentBlock(docker9RouterEntrypointScript, 8)}
2815
3166
  environment:
2816
- - PORT=20128
3167
+ - PORT=${routerPort}
2817
3168
  - HOSTNAME=0.0.0.0
2818
3169
  - CI=true
2819
3170
  volumes:
2820
3171
  - 9router-data:/root/.9router
3172
+ - ./sync.js:/tmp/sync.js:ro
3173
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
2821
3174
  ports:
2822
- - "20128:20128"
3175
+ - "${routerPort}:${routerPort}"
2823
3176
 
2824
3177
  volumes:
2825
3178
  9router-data:`;
@@ -2832,11 +3185,11 @@ services:
2832
3185
  container_name: ${multiAppContainerName}
2833
3186
  restart: always
2834
3187
  env_file:
2835
- - .env
3188
+ - ../../.env
2836
3189
  ${appEnvironmentBlock}${dependsOn}${extraHosts} volumes:
2837
3190
  - ${volumeMount}
2838
3191
  ports:
2839
- - "18791:18791"
3192
+ - "${gatewayPort}:${gatewayPort}"
2840
3193
 
2841
3194
  ollama:
2842
3195
  image: ollama/ollama:latest
@@ -2872,11 +3225,11 @@ services:
2872
3225
  container_name: ${multiAppContainerName}
2873
3226
  restart: always
2874
3227
  env_file:
2875
- - .env
3228
+ - ../../.env
2876
3229
  ${appEnvironmentBlock}${extraHosts} volumes:
2877
3230
  - ${volumeMount}
2878
3231
  ports:
2879
- - "18791:18791"`;
3232
+ - "${gatewayPort}:${gatewayPort}"`;
2880
3233
  }
2881
3234
  } else if (is9Router) {
2882
3235
  compose = `name: ${singleComposeName}
@@ -2886,13 +3239,14 @@ services:
2886
3239
  container_name: ${singleAppContainerName}
2887
3240
  restart: always
2888
3241
  env_file:
2889
- - .env
3242
+ - ../../.env
2890
3243
  depends_on:
2891
3244
  - 9router
2892
- ${appEnvironmentBlock}${hasBrowser ? `${extraHostsBlock}\n` : ''} volumes:
3245
+ ${appEnvironmentBlock}${extraHostsBlock}\n volumes:
2893
3246
  - ${volumeMount}
3247
+ - openclaw-plugins:/root/project/.openclaw/npm
2894
3248
  ports:
2895
- - "18791:18791"
3249
+ - "${gatewayPort}:${gatewayPort}"
2896
3250
 
2897
3251
  9router:
2898
3252
  image: node:22-slim
@@ -2904,16 +3258,19 @@ ${appEnvironmentBlock}${hasBrowser ? `${extraHostsBlock}\n` : ''} volumes:
2904
3258
  - |
2905
3259
  ${indentBlock(docker9RouterEntrypointScript, 8)}
2906
3260
  environment:
2907
- - PORT=20128
3261
+ - PORT=${routerPort}
2908
3262
  - HOSTNAME=0.0.0.0
2909
3263
  - CI=true
2910
3264
  volumes:
2911
3265
  - 9router-data:/root/.9router
3266
+ - ./sync.js:/tmp/sync.js:ro
3267
+ - ./patch-9router.js:/tmp/patch-9router.js:ro
2912
3268
  ports:
2913
- - "20128:20128"
3269
+ - "${routerPort}:${routerPort}"
2914
3270
 
2915
3271
  volumes:
2916
- 9router-data:`;
3272
+ 9router-data:
3273
+ openclaw-plugins:`;
2917
3274
  } else if (isLocal) {
2918
3275
  const ollamaModelTag = String(selectedModel || 'ollama/gemma4:e2b').replace('ollama/', '');
2919
3276
  compose = `name: ${singleComposeName}
@@ -2922,12 +3279,12 @@ services:
2922
3279
  build: .
2923
3280
  container_name: ${singleAppContainerName}
2924
3281
  restart: always
2925
- env_file: .env
3282
+ env_file: ../../.env
2926
3283
  ${appEnvironmentBlock} depends_on:
2927
3284
  ollama:
2928
3285
  condition: service_healthy
2929
- ${hasBrowser ? `${extraHostsBlock}\n` : ''} ports:
2930
- - "18791:18791"
3286
+ ${extraHostsBlock}\n ports:
3287
+ - "${gatewayPort}:${gatewayPort}"
2931
3288
  volumes:
2932
3289
  - ${volumeMount}
2933
3290
 
@@ -2965,17 +3322,19 @@ services:
2965
3322
  container_name: ${singleAppContainerName}
2966
3323
  restart: always
2967
3324
  env_file:
2968
- - .env
3325
+ - ../../.env
2969
3326
  ${appEnvironmentBlock}${plainSingleExtraHosts ? `${extraHostsBlock}\n` : ''} volumes:
2970
3327
  - ${volumeMount}
2971
3328
  ports:
2972
- - "18791:18791"`;
3329
+ - "${gatewayPort}:${gatewayPort}"`;
2973
3330
  }
2974
3331
 
2975
3332
  return {
2976
3333
  dockerfile,
2977
3334
  compose,
3335
+ entrypointScript: runtimeScript,
2978
3336
  syncScript,
3337
+ patchScript,
2979
3338
  docker9RouterEntrypointScript,
2980
3339
  gatewayPatchCmd: buildGatewayPatchCmd(),
2981
3340
  };
@@ -2996,7 +3355,9 @@ if (typeof exports !== 'undefined' && typeof globalThis !== 'undefined' && globa
2996
3355
  Object.assign(exports, globalThis.__openclawDockerGen);
2997
3356
  }
2998
3357
 
2999
- // ── buildNativeScriptCtx and shared native runtime helpers (setup/generators/native-helpers-gen.js)
3358
+
3359
+
3360
+ // ── buildNativeScriptCtx and shared native runtime helpers (setup/generators/native-helpers-gen.js)
3000
3361
  // @ts-nocheck
3001
3362
  /* eslint-disable no-undef, no-unused-vars */
3002
3363
  /**
@@ -3017,10 +3378,9 @@ function buildNativeScriptCtx(options) {
3017
3378
  const ch = CHANNELS[state.channel];
3018
3379
  const is9Router = !!(provider && provider.isProxy);
3019
3380
  const isOllama = !!(provider && provider.isLocal);
3020
- const hasBrowser = state.config.skills.includes('browser');
3021
3381
  const nativeSkillConfigs = state.config.skills
3022
3382
  .map((sid) => SKILLS.find((s) => s.id === sid))
3023
- .filter((skill) => skill && skill.id !== 'scheduler' && skill.slug && skill.slug !== 'browser-automation');
3383
+ .filter((skill) => skill && skill.id !== 'scheduler' && skill.slug);
3024
3384
  const selectedModel = (state.config.model || 'ollama/gemma4:e2b').replace('ollama/', '');
3025
3385
  const isMultiBot = state.botCount > 1 && state.channel === 'telegram';
3026
3386
  const projectDir = state.config.projectPath || '.';
@@ -3040,7 +3400,6 @@ function buildNativeScriptCtx(options) {
3040
3400
  Object.assign(globalThis, {
3041
3401
  isVi,
3042
3402
  provider,
3043
- hasBrowser,
3044
3403
  is9Router,
3045
3404
  selectedModel,
3046
3405
  isMultiBot,
@@ -3048,15 +3407,101 @@ function buildNativeScriptCtx(options) {
3048
3407
  });
3049
3408
 
3050
3409
  function native9RouterSyncScriptContent() {
3051
- return `const fs=require('fs');
3052
- const path=require('path');
3053
- const INTERVAL=30000;
3054
- const p=path.join(process.env.DATA_DIR||'.9router','db.json');
3055
- const ROUTER='${globalThis.__openclawCommon.NINE_ROUTER_API_BASE_URL}';
3056
- const PM=${JSON.stringify(SMART_ROUTE_PROVIDER_MODELS)};
3057
- const PREF=${JSON.stringify(SMART_ROUTE_PROVIDER_ORDER)};
3058
- console.log('[sync-combo] 9Router sync loop started...');
3059
- 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);`;
3410
+ return `const fs = require('fs');
3411
+ const path = require('path');
3412
+ const INTERVAL = 30000;
3413
+ const DB_PATH = path.join(process.env.DATA_DIR || '.9router', 'db', 'data.sqlite');
3414
+ const PORT = process.env.PORT || 20128;
3415
+ const COMBO_NAME = 'smart-route';
3416
+ const API_BASE = \\\`http://localhost:\\\${PORT}\\\`;
3417
+
3418
+ function ensureSettings() {
3419
+ try {
3420
+ let Database;
3421
+ try {
3422
+ const cp = require('child_process');
3423
+ const npmRoot = cp.execSync('npm root -g').toString().trim();
3424
+ Database = require(path.join(npmRoot, '9router', 'node_modules', 'better-sqlite3'));
3425
+ } catch {
3426
+ try { Database = require('better-sqlite3'); } catch { return; }
3427
+ }
3428
+ const db = Database(DB_PATH);
3429
+ const existing = db.prepare("SELECT * FROM settings WHERE id = 1").get();
3430
+ if (!existing) {
3431
+ db.prepare("INSERT INTO settings (id, data) VALUES (1, ?)").run(JSON.stringify({ requireLogin: false }));
3432
+ } else {
3433
+ try {
3434
+ const data = JSON.parse(existing.data || '{}');
3435
+ if (data.requireLogin !== false) {
3436
+ data.requireLogin = false;
3437
+ db.prepare("UPDATE settings SET data = ? WHERE id = 1").run(JSON.stringify(data));
3438
+ }
3439
+ } catch {}
3440
+ }
3441
+ db.close();
3442
+ } catch (e) {}
3443
+ }
3444
+
3445
+ const sync = async () => {
3446
+ try {
3447
+ if (!fs.existsSync(DB_PATH)) return;
3448
+
3449
+ let existingCombo = null;
3450
+ try {
3451
+ const resp = await fetch(\\\`\\\${API_BASE}/api/combos\\\`);
3452
+ if (resp.status === 401) {
3453
+ ensureSettings();
3454
+ return;
3455
+ }
3456
+ const data = await resp.json();
3457
+ if (data.combos) {
3458
+ existingCombo = data.combos.find(c => c.name === COMBO_NAME);
3459
+ }
3460
+ } catch (e) { return; }
3461
+
3462
+ if (existingCombo) return;
3463
+
3464
+ let activeProviders = [];
3465
+ try {
3466
+ const resp = await fetch(\\\`\\\${API_BASE}/api/providers\\\`);
3467
+ const data = await resp.json();
3468
+ const conns = data.connections || data.providerConnections || [];
3469
+ activeProviders = [...new Set(
3470
+ conns.filter(c => c && c.provider && c.isActive !== false && !c.disabled).map(c => c.provider)
3471
+ )];
3472
+ } catch (e) { return; }
3473
+
3474
+ if (!activeProviders.length) return;
3475
+
3476
+ let models = [];
3477
+ try {
3478
+ const resp = await fetch(\\\`\\\${API_BASE}/api/models\\\`);
3479
+ const data = await resp.json();
3480
+ if (data.models && Array.isArray(data.models)) {
3481
+ models = data.models
3482
+ .filter(m => activeProviders.includes(m.provider))
3483
+ .filter(m => !/(embedding|image|tts|stt|audio|vision)/i.test(m.model))
3484
+ .map(m => m.fullModel);
3485
+ }
3486
+ models = [...new Set(models)];
3487
+ } catch (e) { return; }
3488
+
3489
+ if (!models.length) return;
3490
+
3491
+ try {
3492
+ await fetch(\\\`\\\${API_BASE}/api/combos\\\`, {
3493
+ method: 'POST',
3494
+ headers: { 'Content-Type': 'application/json' },
3495
+ body: JSON.stringify({ name: COMBO_NAME, models })
3496
+ });
3497
+ console.log('[sync-combo] Created smart-route with ' + models.length + ' models');
3498
+ } catch (e) {}
3499
+ } catch (e) {}
3500
+ };
3501
+
3502
+ if (fs.existsSync(DB_PATH)) ensureSettings();
3503
+ setTimeout(sync, 10000);
3504
+ setInterval(sync, INTERVAL);`;
3060
3505
  }
3061
3506
 
3062
3507
  function native9RouterServerEntryLookup() {
@@ -3202,12 +3647,16 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3202
3647
  list: multiBotAgentMetas.map((meta) => ({
3203
3648
  id: meta.agentId,
3204
3649
  name: meta.name,
3205
- workspace: '.openclaw/' + meta.workspaceDir,
3206
- agentDir: `agents/${meta.agentId}/agent`,
3207
3650
  model: { primary: state.config.model, fallbacks: [] },
3208
3651
  })),
3209
3652
  },
3210
- commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
3653
+ commands: {
3654
+ native: 'auto',
3655
+ nativeSkills: 'auto',
3656
+ restart: true,
3657
+ ownerDisplay: 'raw',
3658
+ ...(state.config.skills.includes('scheduler') ? { ownerAllowFrom: ['*'] } : {}),
3659
+ },
3211
3660
  bindings: multiBotAgentMetas.map((meta) => ({
3212
3661
  agentId: meta.agentId,
3213
3662
  match: { channel: 'telegram', accountId: meta.accountId },
@@ -3234,6 +3683,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3234
3683
  },
3235
3684
  tools: {
3236
3685
  profile: 'full',
3686
+ ...(state.config.skills.includes('scheduler') ? { alsoAllow: ['group:automation'] } : {}),
3237
3687
  exec: { host: 'gateway', security: 'full', ask: 'off' },
3238
3688
  agentToAgent: {
3239
3689
  enabled: true,
@@ -3254,12 +3704,11 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3254
3704
  }
3255
3705
  } : {}),
3256
3706
  gateway: {
3257
- port: 18791,
3707
+ port: 18789,
3258
3708
  mode: 'local',
3259
- bind: state.nativeOs === 'vps' ? 'custom' : 'loopback',
3260
- ...(state.nativeOs === 'vps' ? { customBindHost: '0.0.0.0' } : {}),
3709
+ bind: state.nativeOs === 'vps' ? 'lan' : 'loopback',
3261
3710
  controlUi: {
3262
- allowedOrigins: getGatewayAllowedOrigins(18791),
3711
+ allowedOrigins: getGatewayAllowedOrigins(18789),
3263
3712
  },
3264
3713
  auth: { mode: 'token', token: crypto.randomUUID().replace(/-/g, '') },
3265
3714
  },
@@ -3304,7 +3753,6 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3304
3753
  provider,
3305
3754
  is9Router,
3306
3755
  isOllama,
3307
- hasBrowser,
3308
3756
  selectedModel,
3309
3757
  isMultiBot,
3310
3758
  projectDir,
@@ -3324,7 +3772,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3324
3772
  };
3325
3773
  }
3326
3774
 
3327
- // ── botEnvContent, botConfigContent, botWorkspaceFiles, botFiles, helpers (setup/generators/config-gen.js)
3775
+ // ── botEnvContent, botConfigContent, botWorkspaceFiles, botFiles, helpers (setup/generators/config-gen.js)
3328
3776
  // @ts-nocheck
3329
3777
  /* eslint-disable no-undef, no-unused-vars */
3330
3778
  /**
@@ -3340,7 +3788,6 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3340
3788
  * @global {boolean} isVi - Vietnamese language mode
3341
3789
  * @global {object} provider - Current primary provider config
3342
3790
  * @global {boolean} isMultiBot - Multi-bot mode flag
3343
- * @global {boolean} hasBrowser - Browser plugin selected
3344
3791
  * @global {boolean} is9Router - 9Router proxy mode
3345
3792
  * @global {string} projectDir - Output project directory path
3346
3793
  * @global {Function} getGatewayAllowedOrigins
@@ -3381,12 +3828,12 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3381
3828
  const bot = state.bots[botIndex] || {};
3382
3829
  const botName = bot.name || `Bot ${botIndex + 1}`;
3383
3830
  const agentId = botName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
3384
- const basePort = 18791 + botIndex;
3831
+ const basePort = 18789 + botIndex;
3385
3832
  const groupId = state.groupId || '';
3386
3833
 
3387
3834
  // Force use global provider if proxy mode is chosen globally, else use bot specific provider
3388
3835
  const botProvider = (provider && provider.isProxy) ? provider : (PROVIDERS[bot.provider] || provider);
3389
- const actualModel = botProvider.isProxy ? provider.models[0].id : (bot.model || state.config.model);
3836
+ const actualModel = botProvider.isProxy ? 'smart-route' : (bot.model || state.config.model);
3390
3837
  const bcfg = globalThis.__openclawBotConfig;
3391
3838
 
3392
3839
  const cfg = bcfg.buildOpenclawJson({
@@ -3405,8 +3852,8 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3405
3852
  groupId,
3406
3853
  selectedSkills: state.config.skills,
3407
3854
  skills: SKILLS,
3408
- hasBrowserDesktop: hasBrowser && state.browserMode === 'desktop',
3409
- hasBrowserServer: hasBrowser && state.browserMode !== 'desktop',
3855
+ hasBrowserDesktop: false,
3856
+ hasBrowserServer: false,
3410
3857
  gatewayPort: basePort,
3411
3858
  gatewayAllowedOrigins: getGatewayAllowedOrigins(basePort),
3412
3859
  osChoice: state.nativeOs || '',
@@ -3476,7 +3923,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3476
3923
  agentWorkspaceDir,
3477
3924
  persona: bot.persona || '',
3478
3925
  userInfo: state.config.userInfo || '',
3479
- hasBrowser,
3926
+ hasBrowser: false,
3480
3927
  soulVariant: 'wizard',
3481
3928
  memoryVariant: 'wizard',
3482
3929
  hasZaloMod: state.channel === 'zalo-personal',
@@ -3554,7 +4001,7 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3554
4001
  }));
3555
4002
  }
3556
4003
 
3557
- // ── generateZaloLoginBat, generateZaloLoginSh (unified) (setup/generators/zalo-login-gen.js)
4004
+ // ── generateZaloLoginBat, generateZaloLoginSh (unified) (setup/generators/zalo-login-gen.js)
3558
4005
  // @ts-nocheck
3559
4006
  /* eslint-disable no-undef, no-unused-vars */
3560
4007
  /**
@@ -3591,15 +4038,13 @@ const sync=async()=>{try{const res=await fetch(ROUTER+'/api/providers');if(!res.
3591
4038
  * @param {string} opts.projectDirVar - BAT var for project dir e.g. '%PROJECT_DIR%'
3592
4039
  * @param {string} opts.label - Unique BAT label suffix (avoid duplicate labels)
3593
4040
  * e.g. 'win', 'multi', 'combo'
3594
- * @param {boolean} [opts.useInstance] - Use --instance default flag (for multi-bot flows)
4041
+ * @param {boolean} [opts.useInstance] - Reserved for QR file suffix only.
3595
4042
  * @returns {string[]} Lines to push into the bat script
3596
4043
  */
3597
4044
  function generateZaloLoginBat(opts) {
3598
4045
  const { homeVar, projectDirVar, label = 'default', useInstance = false } = opts;
3599
4046
  const credPath = `${homeVar}\\credentials\\zalouser\\credentials.json`;
3600
- const loginCmd = useInstance
3601
- ? 'openclaw channels login --channel zalouser --instance default --verbose'
3602
- : 'openclaw channels login --channel zalouser --verbose';
4047
+ const loginCmd = 'openclaw channels login --channel zalouser --verbose';
3603
4048
  const contLabel = `:zalo_continue_${label}`;
3604
4049
  const retryLabel = `:retry_zalo_${label}`;
3605
4050
 
@@ -3639,15 +4084,13 @@ function generateZaloLoginBat(opts) {
3639
4084
  * @param {object} opts
3640
4085
  * @param {string} opts.homeVar - Shell var for OPENCLAW_HOME e.g. '$OPENCLAW_HOME'
3641
4086
  * @param {string} opts.projectDirVar - Shell var for project dir e.g. '$PROJECT_DIR'
3642
- * @param {boolean} [opts.useInstance] - Use --instance default flag
4087
+ * @param {boolean} [opts.useInstance] - Reserved for QR file suffix only.
3643
4088
  * @returns {string[]} Lines to push into the sh script
3644
4089
  */
3645
4090
  function generateZaloLoginSh(opts) {
3646
4091
  const { homeVar, projectDirVar, useInstance = false } = opts;
3647
4092
  const credPath = `${homeVar}/credentials/zalouser/credentials.json`;
3648
- const loginCmd = useInstance
3649
- ? 'openclaw channels login --channel zalouser --instance default --verbose'
3650
- : 'openclaw channels login --channel zalouser --verbose';
4093
+ const loginCmd = 'openclaw channels login --channel zalouser --verbose';
3651
4094
 
3652
4095
  return [
3653
4096
  `# ── Zalo Personal Login (idempotent) ─────────────────────────────────`,
@@ -3671,7 +4114,7 @@ function generateZaloLoginSh(opts) {
3671
4114
  ];
3672
4115
  }
3673
4116
 
3674
- // ── generateStartScript wizard wrapper (delegates to install-gen) (setup/generators/gateway-start-gen.js)
4117
+ // ── generateStartScript wizard wrapper (delegates to install-gen) (setup/generators/gateway-start-gen.js)
3675
4118
  // @ts-nocheck
3676
4119
  /* eslint-disable no-undef, no-unused-vars */
3677
4120
  /**
@@ -3718,7 +4161,7 @@ function generateStartScript() {
3718
4161
  return null;
3719
4162
  }
3720
4163
 
3721
- // ── generateUninstallScript, setup script download helpers (setup/generators/download-gen.js)
4164
+ // ── generateUninstallScript, setup script download helpers (setup/generators/download-gen.js)
3722
4165
  // @ts-nocheck
3723
4166
  /* eslint-disable no-undef, no-unused-vars */
3724
4167
  /**
@@ -3975,7 +4418,7 @@ window.__downloadGen = {
3975
4418
  updateDockerDlLabel,
3976
4419
  };
3977
4420
 
3978
- // ── Windows .bat — if (state.nativeOs === "win") block (setup/os/win-bat.js)
4421
+ // ── Windows .bat — if (state.nativeOs === "win") block (setup/os/win-bat.js)
3979
4422
  // @ts-nocheck
3980
4423
  /* eslint-disable no-undef, no-unused-vars */
3981
4424
  /**
@@ -3985,7 +4428,7 @@ window.__downloadGen = {
3985
4428
  */
3986
4429
  function generateWinBat(ctx) {
3987
4430
  const {
3988
- ch, isVi, provider, is9Router, isOllama, hasBrowser, selectedModel, isMultiBot, projectDir, todayStamp, allPlugins, pluginCmd, nativeSkillInstallCmds, nativeSkillConfigs, providerLines, sharedNativeFileMap, sharedNativeEnvContent, sharedNativeExecApprovalsContent, sharedNativeConfigContent, native9RouterSyncScriptContent, native9RouterServerEntryLookup, windowsHiddenNodeLaunch, generateUninstallScript,
4431
+ ch, isVi, provider, is9Router, isOllama, selectedModel, isMultiBot, projectDir, todayStamp, allPlugins, pluginCmd, nativeSkillInstallCmds, nativeSkillConfigs, providerLines, sharedNativeFileMap, sharedNativeEnvContent, sharedNativeExecApprovalsContent, sharedNativeConfigContent, native9RouterSyncScriptContent, native9RouterServerEntryLookup, windowsHiddenNodeLaunch, generateUninstallScript,
3989
4432
  } = ctx;
3990
4433
  // state, PROVIDERS, SKILLS, PLUGINS, CHANNELS are IIFE-level globals
3991
4434
  let scriptContent;
@@ -4027,8 +4470,8 @@ function generateWinBat(ctx) {
4027
4470
 
4028
4471
  function appendDashboardInfo(arr) {
4029
4472
  arr.push('echo.');
4030
- arr.push('echo OpenClaw Dashboard: http://127.0.0.1:18791');
4031
- arr.push('echo Other reachable URLs: http://localhost:18791');
4473
+ arr.push('echo OpenClaw Dashboard: http://127.0.0.1:18789');
4474
+ arr.push('echo Other reachable URLs: http://localhost:18789');
4032
4475
  arr.push('echo If the dashboard asks for a Gateway Token, run: openclaw dashboard');
4033
4476
  if (is9Router) {
4034
4477
  arr.push('echo.');
@@ -4038,11 +4481,6 @@ function generateWinBat(ctx) {
4038
4481
  }
4039
4482
 
4040
4483
  providerLines(lines, 'bat');
4041
- if (hasBrowser) {
4042
- lines.push('echo Cai Browser Automation runtime...');
4043
- lines.push('call npm install -g agent-browser playwright || goto :fail');
4044
- lines.push('call npx playwright install chromium || goto :fail');
4045
- }
4046
4484
  if (nativeSkillInstallCmds.length > 0) {
4047
4485
  lines.push('echo Cai skills...');
4048
4486
  lines.push(...nativeSkillInstallCmds);
@@ -4120,7 +4558,7 @@ function generateWinBat(ctx) {
4120
4558
  return { scriptName, scriptContent };
4121
4559
  }
4122
4560
 
4123
- // ── macOS .sh — if (state.nativeOs === "macos") block (setup/os/macos-sh.js)
4561
+ // ── macOS .sh — if (state.nativeOs === "macos") block (setup/os/macos-sh.js)
4124
4562
  // @ts-nocheck
4125
4563
  /* eslint-disable no-undef, no-unused-vars */
4126
4564
  /**
@@ -4130,7 +4568,7 @@ function generateWinBat(ctx) {
4130
4568
  */
4131
4569
  function generateMacOsSh(ctx) {
4132
4570
  const {
4133
- ch, isVi, provider, is9Router, isOllama, hasBrowser, selectedModel, isMultiBot, projectDir, todayStamp, allPlugins, pluginCmd, nativeSkillInstallCmds, nativeSkillConfigs, providerLines, sharedNativeFileMap, sharedNativeEnvContent, sharedNativeExecApprovalsContent, sharedNativeConfigContent, native9RouterSyncScriptContent, native9RouterServerEntryLookup, windowsHiddenNodeLaunch, generateUninstallScript,
4571
+ ch, isVi, provider, is9Router, isOllama, selectedModel, isMultiBot, projectDir, todayStamp, allPlugins, pluginCmd, nativeSkillInstallCmds, nativeSkillConfigs, providerLines, sharedNativeFileMap, sharedNativeEnvContent, sharedNativeExecApprovalsContent, sharedNativeConfigContent, native9RouterSyncScriptContent, native9RouterServerEntryLookup, windowsHiddenNodeLaunch, generateUninstallScript,
4134
4572
  } = ctx;
4135
4573
  // state, PROVIDERS, SKILLS, PLUGINS, CHANNELS are IIFE-level globals
4136
4574
  let scriptContent;
@@ -4238,7 +4676,7 @@ function generateMacOsSh(ctx) {
4238
4676
  return { scriptName, scriptContent };
4239
4677
  }
4240
4678
 
4241
- // ── VPS/PM2 .sh — if (state.nativeOs === "vps") block (setup/os/vps-sh.js)
4679
+ // ── VPS/PM2 .sh — if (state.nativeOs === "vps") block (setup/os/vps-sh.js)
4242
4680
  // @ts-nocheck
4243
4681
  /* eslint-disable no-undef, no-unused-vars */
4244
4682
  /**
@@ -4248,7 +4686,7 @@ function generateMacOsSh(ctx) {
4248
4686
  */
4249
4687
  function generateVpsSh(ctx) {
4250
4688
  const {
4251
- ch, isVi, provider, is9Router, isOllama, hasBrowser, selectedModel, isMultiBot, projectDir, todayStamp, allPlugins, pluginCmd, nativeSkillInstallCmds, nativeSkillConfigs, providerLines, sharedNativeFileMap, sharedNativeEnvContent, sharedNativeExecApprovalsContent, sharedNativeConfigContent, native9RouterSyncScriptContent, native9RouterServerEntryLookup, windowsHiddenNodeLaunch, generateUninstallScript,
4689
+ ch, isVi, provider, is9Router, isOllama, selectedModel, isMultiBot, projectDir, todayStamp, allPlugins, pluginCmd, nativeSkillInstallCmds, nativeSkillConfigs, providerLines, sharedNativeFileMap, sharedNativeEnvContent, sharedNativeExecApprovalsContent, sharedNativeConfigContent, native9RouterSyncScriptContent, native9RouterServerEntryLookup, windowsHiddenNodeLaunch, generateUninstallScript,
4252
4690
  } = ctx;
4253
4691
  // state, PROVIDERS, SKILLS, PLUGINS, CHANNELS are IIFE-level globals
4254
4692
  let scriptContent;
@@ -4352,7 +4790,7 @@ GWEOF`);
4352
4790
  }
4353
4791
 
4354
4792
  vps.push('echo ""');
4355
- vps.push('echo "Dashboard: http://127.0.0.1:18791"');
4793
+ vps.push('echo "Dashboard: http://127.0.0.1:18789"');
4356
4794
  if (is9Router) vps.push('echo "9Router: http://127.0.0.1:20128/dashboard"');
4357
4795
  vps.push('echo ""');
4358
4796
  vps.push(`echo "Restart: bash start-bot.sh"`);
@@ -4363,7 +4801,7 @@ GWEOF`);
4363
4801
  return { scriptName, scriptContent };
4364
4802
  }
4365
4803
 
4366
- // ── Linux Desktop .sh — if (state.nativeOs === "linux-desktop") block (setup/os/linux-sh.js)
4804
+ // ── Linux Desktop .sh — if (state.nativeOs === "linux-desktop") block (setup/os/linux-sh.js)
4367
4805
  // @ts-nocheck
4368
4806
  /* eslint-disable no-undef, no-unused-vars */
4369
4807
  /**
@@ -4373,7 +4811,7 @@ GWEOF`);
4373
4811
  */
4374
4812
  function generateLinuxSh(ctx) {
4375
4813
  const {
4376
- ch, isVi, provider, is9Router, isOllama, hasBrowser, selectedModel, isMultiBot, projectDir, todayStamp, allPlugins, pluginCmd, nativeSkillInstallCmds, nativeSkillConfigs, providerLines, sharedNativeFileMap, sharedNativeEnvContent, sharedNativeExecApprovalsContent, sharedNativeConfigContent, native9RouterSyncScriptContent, native9RouterServerEntryLookup, windowsHiddenNodeLaunch, generateUninstallScript,
4814
+ ch, isVi, provider, is9Router, isOllama, selectedModel, isMultiBot, projectDir, todayStamp, allPlugins, pluginCmd, nativeSkillInstallCmds, nativeSkillConfigs, providerLines, sharedNativeFileMap, sharedNativeEnvContent, sharedNativeExecApprovalsContent, sharedNativeConfigContent, native9RouterSyncScriptContent, native9RouterServerEntryLookup, windowsHiddenNodeLaunch, generateUninstallScript,
4377
4815
  } = ctx;
4378
4816
  // state, PROVIDERS, SKILLS, PLUGINS, CHANNELS are IIFE-level globals
4379
4817
  let scriptContent;
@@ -4426,7 +4864,7 @@ function generateLinuxSh(ctx) {
4426
4864
  return { scriptName, scriptContent };
4427
4865
  }
4428
4866
 
4429
- // ── UI init, language/channel/deploy controllers, form rendering (setup/ui/controller.js)
4867
+ // ── UI init, language/channel/deploy controllers, form rendering (setup/ui/controller.js)
4430
4868
  // @ts-nocheck
4431
4869
  /* eslint-disable no-undef, no-unused-vars */
4432
4870
  /**
@@ -5075,7 +5513,7 @@ function generateLinuxSh(ctx) {
5075
5513
  envContent.textContent = lines.join('\n');
5076
5514
  }
5077
5515
 
5078
- // ── Multi-bot state + UI (setup/ui/multi-bot.js) ───────────────────
5516
+ // ── Multi-bot state + UI (setup/ui/multi-bot.js) ───────────────────
5079
5517
  // @ts-nocheck
5080
5518
  /* eslint-disable no-undef, no-unused-vars */
5081
5519
  /**
@@ -5424,7 +5862,7 @@ function generateLinuxSh(ctx) {
5424
5862
 
5425
5863
  // ========== Step 1: Deploy Mode + OS ==========
5426
5864
 
5427
- // ── Step navigation, validation (setup/ui/steps.js) ────────────────
5865
+ // ── Step navigation, validation (setup/ui/steps.js) ────────────────
5428
5866
  // @ts-nocheck
5429
5867
  /* eslint-disable no-undef, no-unused-vars */
5430
5868
  /**
@@ -5584,7 +6022,7 @@ function generateLinuxSh(ctx) {
5584
6022
 
5585
6023
  // ========== Step 2: Bot Config ==========
5586
6024
 
5587
- // ── generateOutput + generateNativeScript + clipboard (setup/ui/output.js)
6025
+ // ── generateOutput + generateNativeScript + clipboard (setup/ui/output.js)
5588
6026
  // @ts-nocheck
5589
6027
  /* eslint-disable no-undef, no-unused-vars */
5590
6028
  /**
@@ -5628,68 +6066,6 @@ function generateLinuxSh(ctx) {
5628
6066
  const routerNotice = document.getElementById('9router-notice');
5629
6067
  if (routerNotice) routerNotice.style.display = is9Router ? '' : 'none';
5630
6068
 
5631
- // Show/hide Browser Automation notice + generate scripts
5632
- const browserNotice = document.getElementById('browser-notice');
5633
- const hasBrowserSkill = state.config.skills.includes('browser');
5634
- if (browserNotice) browserNotice.style.display = hasBrowserSkill ? '' : 'none';
5635
-
5636
- if (hasBrowserSkill) {
5637
- // Chrome Debug .bat script
5638
- const chromeBat = `@echo off
5639
- echo ============================================
5640
- echo OpenClaw - Chrome Debug Mode
5641
- echo ============================================
5642
- echo.
5643
- echo Dang tat Chrome cu (neu co)...
5644
- taskkill /F /IM chrome.exe >nul 2>&1
5645
- timeout /t 3 /nobreak >nul
5646
- echo Dang mo Chrome voi Debug Mode...
5647
- start "" "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" ^
5648
- --remote-debugging-port=9222 ^
5649
- --remote-allow-origins=* ^
5650
- --user-data-dir="%TEMP%\\chrome-debug"
5651
- timeout /t 4 /nobreak >nul
5652
- powershell -Command "try { Invoke-WebRequest -Uri 'http://localhost:9222/json/version' -UseBasicParsing -TimeoutSec 5 | Out-Null; Write-Host 'OK! Chrome Debug Mode dang chay tren port 9222.' -ForegroundColor Green } catch { Write-Host 'LOI: Port 9222 chua mo. Thu lai.' -ForegroundColor Red }"
5653
- echo.
5654
- pause`;
5655
- setOutput('out-chrome-bat', chromeBat);
5656
-
5657
- // Task Scheduler PowerShell script
5658
- const taskPs1 = `# ============================================
5659
- # OpenClaw - Auto-start Chrome Debug khi logon
5660
- # Chay script nay 1 lan voi Run as Administrator
5661
- # ============================================
5662
-
5663
- # Duong dan toi file .bat
5664
- $batPath = "$env:USERPROFILE\\start-chrome-debug.bat"
5665
-
5666
- # Kiem tra file .bat ton tai
5667
- if (-not (Test-Path $batPath)) {
5668
- Write-Host "LOI: Khong tim thay $batPath" -ForegroundColor Red
5669
- Write-Host "Hay luu file start-chrome-debug.bat vao $env:USERPROFILE truoc." -ForegroundColor Yellow
5670
- exit 1
5671
- }
5672
-
5673
- # Tao Scheduled Task
5674
- $action = New-ScheduledTaskAction -Execute $batPath
5675
- $trigger = New-ScheduledTaskTrigger -AtLogOn
5676
- $trigger.Delay = "PT10S" # Delay 10 giay sau khi logon
5677
- $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
5678
-
5679
- Register-ScheduledTask \\
5680
- -TaskName "OpenClaw-ChromeDebug" \\
5681
- -Description "Tu dong bat Chrome Debug Mode cho OpenClaw Browser Automation" \\
5682
- -Action $action \\
5683
- -Trigger $trigger \\
5684
- -Settings $settings \\
5685
- -Force
5686
-
5687
- Write-Host ""
5688
- Write-Host "DONE! Task 'OpenClaw-ChromeDebug' da duoc tao." -ForegroundColor Green
5689
- Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (delay 10s)." -ForegroundColor Cyan`;
5690
- setOutput('out-task-ps1', taskPs1);
5691
- }
5692
-
5693
6069
  // Show/hide docker vs native output based on deployMode
5694
6070
  const dockerOut = document.getElementById('docker-output');
5695
6071
  const nativeOut = document.getElementById('native-output');
@@ -5718,8 +6094,6 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5718
6094
  : 'Script is generated from your choices. Download and run — everything else is handled automatically.';
5719
6095
 
5720
6096
  const agentId = state.config.botName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, '') || 'chat';
5721
-
5722
- const hasBrowser = state.config.skills.includes('browser');
5723
6097
  // isMultiBot => unified into isMultiBot above
5724
6098
  const multiBotAgentMetas = isMultiBot
5725
6099
  ? state.bots.slice(0, state.botCount).map((bot, idx) => {
@@ -5751,15 +6125,26 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5751
6125
  },
5752
6126
  list: [{
5753
6127
  id: agentId,
5754
- workspace: `.openclaw/workspace-${agentId}`,
6128
+ name: botName,
6129
+ workspace: `/root/project/.openclaw/workspace-${agentId}`,
5755
6130
  agentDir: `agents/${agentId}/agent`,
5756
6131
  model: { primary: state.config.model, fallbacks: [] },
5757
6132
  }],
5758
6133
  },
5759
- commands: { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' },
6134
+ commands: {
6135
+ native: 'auto',
6136
+ nativeSkills: 'auto',
6137
+ restart: true,
6138
+ ownerDisplay: 'raw',
6139
+ ...(state.config.skills.includes('scheduler') ? { ownerAllowFrom: ['*'] } : {}),
6140
+ },
5760
6141
  channels: ch.channelConfig,
5761
- tools: { profile: 'full', exec: { host: 'gateway', security: 'full', ask: 'off' } },
5762
- gateway: common.buildGatewayConfig(18791, 'native', getGatewayAllowedOrigins(18791)),
6142
+ tools: {
6143
+ profile: 'full',
6144
+ ...(state.config.skills.includes('scheduler') ? { alsoAllow: ['group:automation'] } : {}),
6145
+ exec: { host: 'gateway', security: 'full', ask: 'off' },
6146
+ },
6147
+ gateway: common.buildGatewayConfig(18789, state.deployMode, getGatewayAllowedOrigins(18789), state.nativeOs || ''),
5763
6148
  };
5764
6149
 
5765
6150
  // 9Router: add proxy endpoint config under models.providers
@@ -5797,19 +6182,6 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5797
6182
  };
5798
6183
  }
5799
6184
 
5800
- // Browser Automation: inject browser config
5801
- if (hasBrowser) {
5802
- clawConfig.browser = {
5803
- enabled: true,
5804
- defaultProfile: 'host-chrome',
5805
- profiles: {
5806
- 'host-chrome': {
5807
- cdpUrl: 'http://127.0.0.1:9222',
5808
- color: '#4285F4',
5809
- },
5810
- },
5811
- };
5812
- }
5813
6185
 
5814
6186
  // Skills: register all selected skills in openclaw.json → skills.entries
5815
6187
  // This makes OpenClaw actually load and enable them at runtime
@@ -5818,8 +6190,6 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5818
6190
  state.config.skills.forEach((sid) => {
5819
6191
  const skill = SKILLS.find((s) => s.id === sid);
5820
6192
  if (!skill) return;
5821
- // Native browser tools are loaded automatically via the root 'browser' config
5822
- if (skill.slug === 'browser-automation') return;
5823
6193
  // scheduler is now native cron (not a skill), skip registering in skills.entries
5824
6194
  if (skill.id === 'scheduler' || !skill.slug) return;
5825
6195
 
@@ -5849,7 +6219,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5849
6219
  clawConfig.agents.list = multiBotAgentMetas.map((meta) => ({
5850
6220
  id: meta.agentId,
5851
6221
  name: meta.name,
5852
- workspace: '.openclaw/' + meta.workspaceDir,
6222
+ workspace: `/root/project/.openclaw/${meta.workspaceDir}`,
5853
6223
  agentDir: `agents/${meta.agentId}/agent`,
5854
6224
  model: { primary: state.config.model, fallbacks: [] },
5855
6225
  }));
@@ -5880,6 +6250,7 @@ Write-Host "Chrome se tu dong bat Debug Mode moi khi ban dang nhap Windows (dela
5880
6250
  };
5881
6251
  clawConfig.tools = {
5882
6252
  ...(clawConfig.tools || {}),
6253
+ ...(state.config.skills.includes('scheduler') ? { alsoAllow: ['group:automation'] } : {}),
5883
6254
  agentToAgent: {
5884
6255
  enabled: true,
5885
6256
  allow: multiBotAgentMetas.map((meta) => meta.agentId),
@@ -5946,7 +6317,7 @@ model:
5946
6317
  const allSkills = [];
5947
6318
  state.config.skills.forEach((sid) => {
5948
6319
  const skill = SKILLS.find((s) => s.id === sid);
5949
- if (skill && skill.slug && skill.slug !== 'browser-automation') {
6320
+ if (skill && skill.slug) {
5950
6321
  allSkills.push(skill.slug);
5951
6322
  }
5952
6323
  });
@@ -5966,7 +6337,6 @@ model:
5966
6337
  is9Router,
5967
6338
  isLocal,
5968
6339
  isMultiBot: state.botCount > 1 && (state.channel === 'telegram'),
5969
- hasBrowser,
5970
6340
  selectedModel: state.config.model || 'ollama/gemma4:e2b',
5971
6341
  agentId: 'bot',
5972
6342
  allSkills,
@@ -5974,6 +6344,9 @@ model:
5974
6344
  dockerfileSkillInstallMode: 'build',
5975
6345
  runtimeCommandParts: [
5976
6346
  pluginInstallCmd,
6347
+ // zalouser: use npm install (not openclaw plugins install) to avoid openclaw.json writes
6348
+ // ClawHub build gives error:not configured; npm version works correctly
6349
+ state.channel === 'zalo-personal' ? 'ensure_zalouser' : '',
5977
6350
  'while true; do sleep 5; openclaw devices approve --latest 2>/dev/null || true; done >/dev/null 2>&1 &'
5978
6351
  ].filter(Boolean),
5979
6352
  plainSingleExtraHosts: true,
@@ -5983,6 +6356,7 @@ model:
5983
6356
  });
5984
6357
  const dockerfile = dockerArtifacts.dockerfile;
5985
6358
  const compose = dockerArtifacts.compose;
6359
+ const entrypointScript = dockerArtifacts.entrypointScript;
5986
6360
  // isMultiBot => unified into isMultiBot above
5987
6361
  setOutput('out-dockerfile', dockerfile);
5988
6362
  setOutput('out-compose', compose);
@@ -6122,12 +6496,6 @@ _This file is yours to evolve. If someone asks to change it, confirm with the us
6122
6496
  `;
6123
6497
 
6124
6498
  // ── AGENTS.md — Hướng dẫn vận hành ("operating manual")
6125
- const browserAgentSection = hasBrowser ? `
6126
- ## Sử dụng Trình Duyệt (Browser Automation)
6127
- - BẠN SỞ HỮU GIAO DIỆN TRÌNH DUYỆT CHROME THẬT CỦA USER thông qua script \`browser-tool.js\`. ĐỌC NGAY FILE \`BROWSER.md\` để biết cách dùng.
6128
- - BẮT BUỘC dùng \`bash\` để gõ \`node ~/browser-tool.js ...\` khi có yêu cầu liên quan đến web thay vì dùng web_search!
6129
- - KHÔNG BAO GIỜ từ chối mở trình duyệt với lý do "không có giao diện" hay "máy chủ không có browser".
6130
- ` : '';
6131
6499
 
6132
6500
  const agentsMd = lang === 'vi'
6133
6501
  ? `# Hướng dẫn vận hành
@@ -6155,7 +6523,6 @@ Bạn hỗ trợ người dùng trong mọi tác vụ hàng ngày thông qua tin
6155
6523
  - Luôn xác nhận kết quả tool trước khi trả lời user
6156
6524
  - Nếu tool lỗi → thông báo rõ ràng, đề xuất cách khác
6157
6525
 
6158
- ${browserAgentSection}
6159
6526
  ${state.config.securityRules}
6160
6527
  `
6161
6528
 
@@ -6239,143 +6606,6 @@ _Update this file as you learn more about the user. Ask before changing._
6239
6606
  // ── MEMORY.md — via scaffold builder
6240
6607
  const memoryMd = _scaffold.buildMemoryDoc({ isVi, variant: 'wizard' });
6241
6608
 
6242
- // Browser tool files (generated into workspace when hasBrowser)
6243
- const browserToolJs = `/**
6244
- * browser-tool.js - Connect to real Windows Chrome via CDP
6245
- * Flow: Docker -> socat (port 9222) -> host.docker.internal:9222 -> user's Chrome
6246
- */
6247
- const { chromium } = require('/usr/local/lib/node_modules/openclaw/node_modules/playwright-core');
6248
- const action = process.argv[2];
6249
- const param1 = process.argv[3];
6250
- const param2 = process.argv[4];
6251
- const CDP_URL = 'http://127.0.0.1:9222';
6252
- (async () => {
6253
- let browser;
6254
- try {
6255
- browser = await chromium.connectOverCDP(CDP_URL, { timeout: 5000 });
6256
- const ctx = browser.contexts()[0];
6257
- const pages = ctx.pages();
6258
- let page = pages.length > 0 ? pages[0] : await ctx.newPage();
6259
- if (action === 'open') {
6260
- console.log('[Browser] Mo trang: ' + param1);
6261
- await page.goto(param1, { waitUntil: 'domcontentloaded', timeout: 30000 });
6262
- await page.waitForTimeout(1500);
6263
- console.log('[Browser] Da mo: ' + (await page.title()) + ' | ' + page.url());
6264
- } else if (action === 'get_text') {
6265
- const text = await page.evaluate(() => {
6266
- document.querySelectorAll('script,style,noscript,svg').forEach(e => e.remove());
6267
- return document.body.innerText.trim();
6268
- });
6269
- console.log(text.substring(0, 4000));
6270
- } else if (action === 'click') {
6271
- await page.locator(param1).first().click({ timeout: 5000 });
6272
- await page.waitForTimeout(600);
6273
- console.log('[Browser] Da click: ' + param1);
6274
- } else if (action === 'fill') {
6275
- await page.locator(param1).first().fill(param2, { timeout: 5000 });
6276
- console.log('[Browser] Da dien "' + param2 + '" vao: ' + param1);
6277
- } else if (action === 'press') {
6278
- await page.keyboard.press(param1);
6279
- await page.waitForTimeout(1000);
6280
- console.log('[Browser] Da nhan phim: ' + param1);
6281
- } else if (action === 'status') {
6282
- console.log('[Browser] Ket noi Chrome that! Tab: ' + (await page.title()) + ' | ' + page.url());
6283
- } else {
6284
- console.log('Lenh: open <url> | get_text | click <sel> | fill <sel> <text> | press <key> | status');
6285
- }
6286
- } catch(e) {
6287
- if (e.message.includes('ECONNREFUSED') || e.message.includes('Timeout')) {
6288
- console.error('[Browser] Ban chua bat Chrome Debug! Hay click dup vao start-chrome-debug.bat roi thu lai.');
6289
- } else {
6290
- console.error('[Browser] Loi:', e.message);
6291
- }
6292
- } finally {
6293
- if (browser) await browser.close();
6294
- }
6295
- `;
6296
-
6297
- const browserMd = `# Dieu Khien Trinh Duyet (Browser Automation)
6298
-
6299
- Bot dieu khien Chrome THAT tren man hinh Windows cua ban. Moi thao tac hien thi truc tiep!
6300
-
6301
- ## Lenh su dung (chay qua bash)
6302
-
6303
- \\\`\\\`\\\`bash
6304
- node ~/browser-tool.js status
6305
- node ~/browser-tool.js open "https://google.com"
6306
- node ~/browser-tool.js get_text
6307
- node ~/browser-tool.js fill "input[name='q']" "tu khoa"
6308
- node ~/browser-tool.js press "Enter"
6309
- node ~/browser-tool.js click "#button"
6310
- \\\`\\\`\\\`
6311
-
6312
- ## QUY TAC BAT BUOC
6313
- - KHONG BAO GIO tu choi mo trinh duyet.
6314
- - Neu loi ECONNREFUSED: bao user bat lai start-chrome-debug.bat.
6315
- - KHONG dung web_search khi user yeu cau browser.
6316
- `;
6317
-
6318
- const chromeBatContent = `@echo off
6319
- echo ====== OpenClaw - Chrome Debug Mode ======
6320
- echo.
6321
- echo Dang tat Chrome cu (neu co)...
6322
- taskkill /F /IM chrome.exe >nul 2>&1
6323
- timeout /t 3 /nobreak >nul
6324
- echo Dang mo Chrome voi Debug Mode...
6325
- start "" "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" ^
6326
- --remote-debugging-port=9222 ^
6327
- --remote-allow-origins=* ^
6328
- --user-data-dir="%TEMP%\\chrome-debug"
6329
- timeout /t 4 /nobreak >nul
6330
- powershell -Command "try { Invoke-WebRequest -Uri 'http://localhost:9222/json/version' -UseBasicParsing -TimeoutSec 5 | Out-Null; Write-Host 'OK! Chrome Debug Mode dang chay.' -ForegroundColor Green } catch { Write-Host 'LOI: Port 9222 chua mo.' -ForegroundColor Red }"
6331
- echo.
6332
- pause
6333
- `;
6334
-
6335
- const chromeShContent = `#!/usr/bin/env bash
6336
- # ====== OpenClaw - Chrome Debug Mode (Mac/Linux) ======
6337
- set -e
6338
- echo "====== OpenClaw - Chrome Debug Mode ======"
6339
- echo ""
6340
-
6341
- # Detect Chrome path
6342
- if [[ "$OSTYPE" == "darwin"* ]]; then
6343
- CHROME_BIN="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
6344
- [ ! -f "$CHROME_BIN" ] && CHROME_BIN="/Applications/Chromium.app/Contents/MacOS/Chromium"
6345
- [ ! -f "$CHROME_BIN" ] && CHROME_BIN="/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
6346
- else
6347
- CHROME_BIN="$(command -v google-chrome || command -v google-chrome-stable || command -v chromium-browser || command -v chromium || echo '')"
6348
- fi
6349
- [ -n "$CHROME_DEBUG_BIN" ] && CHROME_BIN="$CHROME_DEBUG_BIN"
6350
-
6351
- if [ -z "$CHROME_BIN" ] || { [ ! -f "$CHROME_BIN" ] && [ ! -x "$CHROME_BIN" ]; }; then
6352
- echo -e "\\033[31mERROR: Chrome/Chromium not found.\\033[0m"
6353
- echo "Install Chrome or: export CHROME_DEBUG_BIN=/path/to/chrome"
6354
- exit 1
6355
- fi
6356
-
6357
- echo "Using: $CHROME_BIN"
6358
- echo "Killing existing Chrome debug instances..."
6359
- pkill -f -- "--remote-debugging-port=9222" 2>/dev/null || true
6360
- sleep 2
6361
-
6362
- TMP_DIR="\${TMPDIR:-/tmp}/chrome-debug-openclaw"
6363
- mkdir -p "$TMP_DIR"
6364
-
6365
- echo "Starting Chrome in Debug Mode (port 9222)..."
6366
- "$CHROME_BIN" \\
6367
- --remote-debugging-port=9222 \\
6368
- --remote-allow-origins=* \\
6369
- --user-data-dir="$TMP_DIR" &
6370
-
6371
- sleep 4
6372
- if curl -s http://localhost:9222/json/version > /dev/null 2>&1; then
6373
- echo -e "\\033[32mOK! Chrome Debug Mode is running on port 9222.\\033[0m"
6374
- else
6375
- echo -e "\\033[31mERROR: Port 9222 not responding.\\033[0m"
6376
- exit 1
6377
- fi
6378
- `;
6379
6609
 
6380
6610
  const envText = (document.getElementById('env-content')?.textContent || '').trim();
6381
6611
  const rootEnvContent = envText ? `${envText}\n` : '';
@@ -6396,6 +6626,7 @@ fi
6396
6626
  if (!isNativeMode) {
6397
6627
  sharedFiles['docker/openclaw/Dockerfile'] = dockerfile;
6398
6628
  sharedFiles['docker/openclaw/docker-compose.yml'] = compose;
6629
+ sharedFiles['docker/openclaw/entrypoint.sh'] = entrypointScript;
6399
6630
  sharedFiles['docker/openclaw/.env'] = rootEnvContent;
6400
6631
  }
6401
6632
  sharedFiles[globalThis.__openclawCommon.TELEGRAM_SETUP_GUIDE_FILENAME] = buildTelegramPostInstallChecklist();
@@ -6429,10 +6660,6 @@ fi
6429
6660
  });
6430
6661
  sharedFiles[`.openclaw/${meta.workspaceDir}/TEAMS.md`] = _scaffold.buildTeamsDoc({ isVi });
6431
6662
  sharedFiles[`.openclaw/${meta.workspaceDir}/MEMORY.md`] = memoryMd;
6432
- if (hasBrowser) {
6433
- sharedFiles[`.openclaw/${meta.workspaceDir}/browser-tool.js`] = browserToolJs;
6434
- sharedFiles[`.openclaw/${meta.workspaceDir}/BROWSER.md`] = browserMd;
6435
- }
6436
6663
  }
6437
6664
  state._generatedFiles = sharedFiles;
6438
6665
  } else {
@@ -6452,10 +6679,6 @@ fi
6452
6679
  }),
6453
6680
  [`.openclaw/workspace-${agentId}/MEMORY.md`]: memoryMd,
6454
6681
  '.gitignore': isNativeMode ? '.env\nnode_modules/' : '.env\ndocker/openclaw/.env\nnode_modules/',
6455
- ...(hasBrowser ? {
6456
- [`.openclaw/workspace-${agentId}/browser-tool.js`]: browserToolJs,
6457
- [`.openclaw/workspace-${agentId}/BROWSER.md`]: browserMd,
6458
- } : {}),
6459
6682
  };
6460
6683
  if (rootEnvContent) {
6461
6684
  singleFiles['.env'] = rootEnvContent;
@@ -6467,6 +6690,7 @@ fi
6467
6690
  if (!isNativeMode) {
6468
6691
  singleFiles['docker/openclaw/Dockerfile'] = dockerfile;
6469
6692
  singleFiles['docker/openclaw/docker-compose.yml'] = compose;
6693
+ singleFiles['docker/openclaw/entrypoint.sh'] = entrypointScript;
6470
6694
  singleFiles['docker/openclaw/.env'] = rootEnvContent;
6471
6695
  }
6472
6696
  state._generatedFiles = singleFiles;
@@ -6476,8 +6700,6 @@ fi
6476
6700
  // chrome-debug, start-bot, uninstall added ONCE here, not per-bot-mode block
6477
6701
  if (isNativeMode) {
6478
6702
  const _files = state._generatedFiles;
6479
- _files['start-chrome-debug.bat'] = chromeBatContent;
6480
- _files['start-chrome-debug.sh'] = chromeShContent;
6481
6703
  _files['start-bot.bat'] = generateStartBotBat({
6482
6704
  projectDir: state.config.projectPath || '.',
6483
6705
  openclawHome: (state.config.projectPath || '.') + '\\.openclaw',
@@ -6635,38 +6857,46 @@ fi
6635
6857
  // ========== Zalo Personal Login Guide (post-setup) ==========
6636
6858
  function generateZaloOnboardGuide() {
6637
6859
  const lang = document.getElementById('cfg-language')?.value || 'vi';
6638
- setOutput('out-zalo-onboard-cmd', `docker compose stop ai-bot
6639
- docker compose run --rm --no-deps ai-bot openclaw channels login --channel zalouser --verbose
6640
- docker compose up -d --force-recreate ai-bot
6641
- docker compose exec ai-bot openclaw channels status --probe`);
6860
+ const isVi = lang === 'vi';
6861
+ const containerName = state.botCount > 1 ? 'openclaw-multibot' : 'openclaw-bot';
6862
+
6863
+ setOutput('out-zalo-onboard-cmd', `# ${isVi ? 'Bước 1: Dọn dẹp session cũ' : 'Step 1: Clean up old session'}
6864
+ docker exec ${containerName} rm -f /root/project/.openclaw/credentials/zalouser/credentials.json
6642
6865
 
6643
- if (lang === 'vi') {
6866
+ # ${isVi ? 'Bước 2: Kích hoạt màn hình login QR (Quét trên terminal)' : 'Step 2: Start login QR (Scan on terminal)'}
6867
+ docker exec -it ${containerName} openclaw channels login --channel zalouser --verbose
6868
+
6869
+ # ${isVi ? 'Bước 3: Khởi động lại container sau khi login thành công' : 'Step 3: Restart container after successful login'}
6870
+ docker restart ${containerName}`);
6871
+
6872
+ if (isVi) {
6644
6873
  setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
6645
- │ Chạy lệnh bên trái để OpenClaw tạo QR đăng nhập.
6874
+ │ Chạy các lệnh bên trái để đăng nhập Zalo Personal
6875
+ │ theo quy trình chuẩn 4 bước. │
6646
6876
  ├─────────────────────────────────────────────────────┤
6647
- │ 1. Đảm bảo container/gateway đã chạy xong.
6648
- 2. Stop ai-bot, login bằng compose run one-shot.
6649
- 3. OpenClaw sẽ in ra đường dẫn file QR trong /tmp.
6650
- 4. Copy file QR ra ngoài nếu cần:
6651
- │ docker cp openclaw-bot:/tmp/openclaw/ │
6877
+ │ 1. Lệnh 1 xoá file credentials.json cũ để tránh
6878
+ lỗi xung đột "Already linked".
6879
+ 2. Lệnh 2 mở màn hình login. Quét QR hiện trên
6880
+ terminal hoặc lấy file từ container:
6881
+ │ docker cp ${containerName}:/tmp/openclaw/ │
6652
6882
  │ openclaw-zalouser-qr-default.png . │
6653
- 5. Mở ảnh QR → quét bằng app Zalo xác nhận.
6654
- 6. Start lại: docker compose up -d --force-recreate
6655
- 7. Chạy channels status --probe, phải thấy running.│
6883
+ 3. Sau khi quét xong terminal báo thành công,
6884
+ nhấn Ctrl+C để thoát.
6885
+ 4. Chạy Lệnh 3 để restart container giúp nhận tin. │
6656
6886
  └─────────────────────────────────────────────────────┘`);
6657
6887
  } else {
6658
6888
  setOutput('out-zalo-onboard-guide', `┌─────────────────────────────────────────────────────┐
6659
- │ Run the command on the left to generate a Zalo QR.
6889
+ │ Run the commands on the left to login Zalo Personal
6890
+ │ following the standard 4-step workflow. │
6660
6891
  ├─────────────────────────────────────────────────────┤
6661
- │ 1. Make sure the container/gateway is already up.
6662
- 2. Stop ai-bot; login with compose run one-shot.
6663
- 3. OpenClaw prints the QR image path under /tmp.
6664
- 4. Copy the QR out if needed:
6665
- │ docker cp openclaw-bot:/tmp/openclaw/ │
6892
+ │ 1. Command 1 deletes the old credentials.json file
6893
+ to avoid "Already linked" conflicts.
6894
+ 2. Command 2 opens the login interface. Scan the
6895
+ QR on your terminal or copy the image file:
6896
+ │ docker cp ${containerName}:/tmp/openclaw/ │
6666
6897
  │ openclaw-zalouser-qr-default.png . │
6667
- 5. Open the image scan with Zalo mobile app.
6668
- 6. Start again: docker compose up -d --force-re...
6669
- │ 7. Run channels status --probe; it should run. │
6898
+ 3. Once scanned and successful, press Ctrl+C.
6899
+ 4. Run Command 3 to restart the container.
6670
6900
  └─────────────────────────────────────────────────────┘`);
6671
6901
  }
6672
6902
  }