create-openclaw-bot 5.14.0 → 5.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -183,6 +183,26 @@
183
183
  };
184
184
  }
185
185
 
186
+ // ── messages (ack reaction) ───────────────────────────────────────────────
187
+ // Drop a reaction on inbound messages so people can see the bot registered theirs
188
+ // before the reply lands. This happens in the channel transport — the model is never
189
+ // involved, so it costs no tokens.
190
+ //
191
+ // Zalo-only, and deliberately so: zalo-connect reads this GLOBAL key (it has no
192
+ // per-channel ackReaction of its own), but the same key also feeds Telegram, Discord,
193
+ // Slack and WhatsApp, which each accept only their own fixed reaction set. Seeding an
194
+ // arbitrary emoji globally would make those channels reject every ack, so other
195
+ // channels keep their existing behaviour until someone picks a value valid there.
196
+ //
197
+ // 🦞 is sent as a Zalo *custom* reaction (emoji as rIcon + a hash as rType), which
198
+ // needs zalo-connect ≥3.0.15 — earlier versions only knew the 55 built-ins and would
199
+ // drop it silently. Scope "all" covers DMs and groups; in a mention-gated group the
200
+ // plugin still only reacts to messages that address the bot, since non-addressed ones
201
+ // are buffered as passive context before the reaction step.
202
+ if (isZaloPersonal(channelKey)) {
203
+ cfg.messages = { ackReaction: '🦞', ackReactionScope: 'all', removeAckAfterReply: false };
204
+ }
205
+
186
206
  // ── commands ──────────────────────────────────────────────────────────────
187
207
  cfg.commands = { native: 'auto', nativeSkills: 'auto', restart: true, ownerDisplay: 'raw' };
188
208
  if (selectedSkills.includes('scheduler')) {
@@ -228,6 +248,18 @@
228
248
  if (alsoAllow.length > 0) {
229
249
  cfg.tools.alsoAllow = alsoAllow;
230
250
  }
251
+ // Hide OpenClaw's native `browser` tool: browsing goes through the browser-automation
252
+ // plugin's own CLI (skills/browser-automation/browser-tool.js), which adds the page-
253
+ // reading commands the native tool lacks (get_text, get_links) — that is the whole
254
+ // point of shipping the plugin. Left visible, the model reaches for the native tool
255
+ // instead and asks it for the container-local "openclaw" profile, which has no browser
256
+ // behind it, then reports "no browser available" while the real one sits unused.
257
+ // The bundled `browser` plugin stays allowed (it provides the browser-control service);
258
+ // only the tool is denied, and browser-tool.js drives Chrome directly over CDP.
259
+ if (hasBrowserDesktop || hasBrowserServer
260
+ || selectedSkills.includes('browser') || selectedSkills.includes('browser-automation')) {
261
+ cfg.tools.deny = ['browser'];
262
+ }
231
263
  // DuckDuckGo is the bundled, credential-free web_search provider. Auto-detect only
232
264
  // picks providers that have credentials, so a free provider must be selected
233
265
  // explicitly, otherwise web_search reports "no provider is available".
@@ -266,11 +266,14 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
266
266
  // created/regenerated — so a plain rebuild would never pick them up):
267
267
  // • skills.workshop.approvalPolicy:'auto' → the assistant can author a workspace
268
268
  // skill end-to-end on request instead of stopping at "proposal awaiting approval".
269
+ // • tools.deny gains `browser` on projects that have browsing enabled, so the model
270
+ // stops reaching for the native tool instead of the plugin's browser-tool.js.
271
+ // (messages.ackReaction is handled separately, below, because it is Zalo-only.)
269
272
  // • imageMaxDimensionPx / imageQuality / contextLimits.toolResultMaxChars → keep one
270
273
  // heavy turn (deep research, 4K chart read-back) from overflowing the context
271
274
  // window mid tool-loop, which cannot be compacted and poisons the session.
272
275
  // Each key is only filled in when absent, so an operator's own tuning is never clobbered.
273
- const contextDefaultsScript = `const fs=require('fs'),path=require('path');const p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));let ch=false;c.skills=c.skills||{};c.skills.workshop=c.skills.workshop||{};if(!c.skills.workshop.approvalPolicy){c.skills.workshop.approvalPolicy='auto';ch=true;}const d=(c.agents&&c.agents.defaults)?c.agents.defaults:null;if(d){if(d.imageMaxDimensionPx===undefined){d.imageMaxDimensionPx=1024;ch=true;}if(d.imageQuality===undefined){d.imageQuality='efficient';ch=true;}d.contextLimits=d.contextLimits||{};if(d.contextLimits.toolResultMaxChars===undefined){d.contextLimits.toolResultMaxChars=12000;ch=true;}}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
276
+ const contextDefaultsScript = `const fs=require('fs'),path=require('path');const p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));let ch=false;c.skills=c.skills||{};c.skills.workshop=c.skills.workshop||{};if(!c.skills.workshop.approvalPolicy){c.skills.workshop.approvalPolicy='auto';ch=true;}if(c.browser&&c.browser.enabled!==false){c.tools=c.tools||{};const dn=Array.isArray(c.tools.deny)?c.tools.deny:[];if(!dn.includes('browser')){dn.push('browser');c.tools.deny=dn;ch=true;}}const d=(c.agents&&c.agents.defaults)?c.agents.defaults:null;if(d){if(d.imageMaxDimensionPx===undefined){d.imageMaxDimensionPx=1024;ch=true;}if(d.imageQuality===undefined){d.imageQuality='efficient';ch=true;}d.contextLimits=d.contextLimits||{};if(d.contextLimits.toolResultMaxChars===undefined){d.contextLimits.toolResultMaxChars=12000;ch=true;}}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
274
277
  // Companion backfill for the same older projects: their TOOLS.md was generated before the
275
278
  // skill-authoring / long-turn guidance existed, and workspace files are only written when a
276
279
  // bot is created — so a rebuild alone leaves the assistant stopping at "proposal awaiting
@@ -344,6 +347,13 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
344
347
  // native ZaloConnect actions.
345
348
  const zaloConnectSpec = common.ZALO_CONNECT_PLUGIN_SPEC || 'clawhub:openclaw-zalo-connect';
346
349
  runtimeParts.push(`ensure_plugin zalo-connect "${zaloConnectSpec}"`);
350
+ // Backfill the inbound-message ack reaction for projects created before it was
351
+ // seeded. Kept out of the shared migration because zalo-connect reads the GLOBAL
352
+ // messages.ackReaction, and that same key also feeds Telegram/Discord/Slack/
353
+ // WhatsApp — which accept only their own fixed reaction sets and would reject an
354
+ // arbitrary emoji. Absent-only, so an operator's own choice is never overwritten.
355
+ const ackReactionScript = `const fs=require('fs'),path=require('path');const p=path.join(process.cwd(),'.openclaw','openclaw.json');if(fs.existsSync(p)){const c=JSON.parse(fs.readFileSync(p,'utf8'));let ch=false;c.messages=c.messages||{};if(c.messages.ackReaction===undefined){c.messages.ackReaction='🦞';ch=true;}if(c.messages.ackReactionScope===undefined){c.messages.ackReactionScope='all';ch=true;}if(ch)fs.writeFileSync(p,JSON.stringify(c,null,2));}`;
356
+ runtimeParts.push(`node - <<'NODE'\n${ackReactionScript}\nNODE`);
347
357
  }
348
358
  // Always-on memory context engine for every bot (see bot-config-gen: plugins.slots
349
359
  // .contextEngine = "learning-memory"). ensure_plugin skips if already installed.
@@ -355,6 +365,77 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
355
365
  // Backfill skill-authoring + context defaults for configs from an older setup (see above).
356
366
  runtimeParts.push(`node - <<'NODE'\n${contextDefaultsScript}\nNODE`);
357
367
  runtimeParts.push(`node - <<'NODE'\n${agentsGuidanceScript}\nNODE`);
368
+ // browser-tool.js is a CDP client only — it has no code to launch a browser, so it
369
+ // needs something listening on a debug port. On a desktop that is the operator's own
370
+ // Chrome (started by start-chrome), reached through the host gateway. On a server there
371
+ // is no such Chrome, and browsing simply failed. Start a headless Chromium on loopback
372
+ // 9222 — the second entry in browser-tool.js's candidate list — so the same tool works
373
+ // on every OS.
374
+ //
375
+ // Emitted unconditionally and gated at RUNTIME on the plugin being installed, not on
376
+ // hasBrowser: the dashboard creates projects without that flag, so an operator who turns
377
+ // browser-automation on later (the common path) would otherwise never get this block.
378
+ //
379
+ // Chromium is baked into the image only when hasBrowser was known at build time. When it
380
+ // is missing — every project whose image predates the plugin — download it once instead
381
+ // of telling the bot to give up; that message is what makes it answer "there is no
382
+ // browser in my environment". The download goes to a path under $OPENCLAW_HOME, which is
383
+ // a bind mount, so recreating the container does not pay for it again. It runs in the
384
+ // background: the gateway must not wait ~150MB before answering messages.
385
+ //
386
+ // Skipped when a host Chrome is already reachable (it is tried first anyway) or when
387
+ // something already holds 9222, so a desktop does not pay for an idle browser.
388
+ runtimeParts.push([
389
+ // Not exported: the plugin (and anything else in the image) resolves Playwright's own
390
+ // cache, and pointing that at an empty directory would break a Chromium that IS baked in.
391
+ 'openclaw_browsers_dir="$OPENCLAW_HOME/browsers"',
392
+ // The installed plugin folder, not the config: browsing needs browser-tool.js, which
393
+ // ships with the plugin, so "enabled in config but never installed" has nothing to serve.
394
+ 'browser_automation_enabled() {',
395
+ ' [ -d "$OPENCLAW_HOME/extensions/browser-automation" ]',
396
+ '}',
397
+ 'find_chrome_bin() {',
398
+ ' for candidate in /usr/bin/google-chrome /usr/bin/chromium /usr/bin/chromium-browser; do',
399
+ ' [ -x "$candidate" ] && echo "$candidate" && return 0',
400
+ ' done',
401
+ ' ls -d "$openclaw_browsers_dir"/chromium-*/chrome-linux*/chrome "$HOME"/.cache/ms-playwright/chromium-*/chrome-linux*/chrome /root/.cache/ms-playwright/chromium-*/chrome-linux*/chrome 2>/dev/null | head -n 1',
402
+ '}',
403
+ 'launch_headless_chrome() {',
404
+ ' echo "[entrypoint] starting local headless Chromium on 127.0.0.1:9222"',
405
+ ' "$1" --headless=new --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1 \\',
406
+ ' --no-sandbox --disable-dev-shm-usage --disable-gpu --no-first-run --no-default-browser-check \\',
407
+ ' --user-data-dir=/tmp/openclaw-headless-chrome >/tmp/openclaw-headless-chrome.log 2>&1 &',
408
+ '}',
409
+ 'start_local_headless_chrome() {',
410
+ ' browser_automation_enabled || return 0',
411
+ ' if curl -s -m 2 http://127.0.0.1:9222/json/version >/dev/null 2>&1; then return 0; fi',
412
+ ' host_ip="$(getent hosts host.docker.internal 2>/dev/null | awk \'{print $1}\' | head -n 1)"',
413
+ ' if [ -n "$host_ip" ] && curl -s -m 2 "http://$host_ip:9222/json/version" >/dev/null 2>&1; then',
414
+ ' echo "[entrypoint] host Chrome reachable at $host_ip:9222; not starting a local one"',
415
+ ' return 0',
416
+ ' fi',
417
+ ' chrome_bin="$(find_chrome_bin || true)"',
418
+ ' if [ -n "$chrome_bin" ] && [ -x "$chrome_bin" ]; then',
419
+ ' launch_headless_chrome "$chrome_bin"',
420
+ ' return 0',
421
+ ' fi',
422
+ ' echo "[entrypoint] browser-automation is on but this image has no Chromium; downloading it once (~150MB) in the background"',
423
+ ' (',
424
+ ' mkdir -p "$openclaw_browsers_dir"',
425
+ ' PLAYWRIGHT_BROWSERS_PATH="$openclaw_browsers_dir" npx --yes playwright install --with-deps chromium >/tmp/openclaw-chromium-install.log 2>&1 \\',
426
+ ' || PLAYWRIGHT_BROWSERS_PATH="$openclaw_browsers_dir" npx --yes playwright install chromium >>/tmp/openclaw-chromium-install.log 2>&1',
427
+ ' installed_bin="$(find_chrome_bin || true)"',
428
+ ' if [ -n "$installed_bin" ] && [ -x "$installed_bin" ]; then',
429
+ ' echo "[entrypoint] Chromium ready at $installed_bin"',
430
+ ' launch_headless_chrome "$installed_bin"',
431
+ ' else',
432
+ ' echo "[entrypoint] Chromium download failed; see /tmp/openclaw-chromium-install.log — browsing still works if the operator runs start-chrome on the host"',
433
+ ' fi',
434
+ ' ) &',
435
+ '}',
436
+ // `set -e` is on: a non-zero return here must never stop the gateway from starting.
437
+ 'start_local_headless_chrome || true',
438
+ ].join('\n'));
358
439
  runtimeParts.push('openclaw gateway run');
359
440
  const runtimeScript = ['#!/bin/sh', 'set -e', ...runtimeParts].join('\n');
360
441
  let browserInstall = '';
@@ -1180,9 +1180,15 @@ description: Guide for utilizing tools and skills
1180
1180
  : (isVi
1181
1181
  ? `Reaction phải nhắm vào **tin nhắn mới nhất của user** (inbound hiện tại), KHÔNG phải tin cũ hơn hay tin của chính mình.\n- Cách đúng: tool \`message\` với \`action: "react"\`, ${reactValue}, **KHÔNG truyền \`messageId\`** — tool sẽ tự reaction vào inbound message mới nhất. Chỉ truyền \`messageId\` khi cần reaction vào một tin CỤ THỂ không phải tin mới nhất.`
1182
1182
  : `The reaction MUST target the **user's latest (current inbound) message**, not an older one or your own message.\n- Correct: \`message\` tool with \`action: "react"\`, ${reactValue}, **do NOT pass \`messageId\`** — the tool auto-reacts to the latest inbound message. Only pass \`messageId\` to react to a specific OLDER message.`);
1183
- const dmOverride = isVi
1184
- ? `\n\n## Reaction khi nhắn riêng (DM) — BẮT BUỘC\n- Khi DM với user, PHẢI luôn dùng tool/action reaction native để thả reaction CÙNG LÚC khi trả lời.\n- KHÔNG thả reaction trong group chat.\n- Chọn theo cảm xúc/ngữ cảnh tin của user. ${reactList}\n- ${reactHow}\n- ${reactExamples}`
1185
- : `\n\n## Reactions in DMs MANDATORY\n- In DMs with the user, you MUST always use the native reaction tool/action to react WHILE replying.\n- Do NOT react in group chats.\n- Pick by the user's emotion/context. ${reactList}\n- ${reactHow}\n- ${reactExamples}`;
1183
+ // Zalo bots no longer carry this: the channel now drops an ack reaction on every
1184
+ // inbound message by itself (messages.ackReaction), so telling the model to react
1185
+ // as well only spends tokens and risks a second reaction on the same message.
1186
+ // Channels without that automatic ack keep the instruction.
1187
+ const dmOverride = isZalo
1188
+ ? ''
1189
+ : isVi
1190
+ ? `\n\n## ⚡ Reaction khi nhắn riêng (DM) — BẮT BUỘC\n- Khi DM với user, PHẢI luôn dùng tool/action reaction native để thả reaction CÙNG LÚC khi trả lời.\n- KHÔNG thả reaction trong group chat.\n- Chọn theo cảm xúc/ngữ cảnh tin của user. ${reactList}\n- ${reactHow}\n- ${reactExamples}`
1191
+ : `\n\n## ⚡ Reactions in DMs — MANDATORY\n- In DMs with the user, you MUST always use the native reaction tool/action to react WHILE replying.\n- Do NOT react in group chats.\n- Pick by the user's emotion/context. ${reactList}\n- ${reactHow}\n- ${reactExamples}`;
1186
1192
 
1187
1193
  // Doc structure mirrors OpenClaw's default TOOLS.md (local environment notes + why it is a
1188
1194
  // separate file), followed by the repo's tool-usage rules and the mandatory DM reaction guide.
package/dist/web/app.js CHANGED
@@ -7,9 +7,13 @@ const OS_OPTIONS = [
7
7
  { id: 'linux-desktop', title: 'Linux Desktop', subtitle: 'Ubuntu / Debian / Fedora', icon: `${SVG_CDN}/linux/default.svg`, badge: 'Desktop' },
8
8
  { id: 'vps', title: 'Linux VPS', subtitle: 'Server install with public bind', icon: `${SVG_CDN}/ubuntu/default.svg`, badge: 'Server' },
9
9
  ];
10
- // Docker is the only supported deploy mode. (Native mode was removed.)
10
+ // Two deploy modes. Docker isolates the bot in a container; native runs openclaw + 9router
11
+ // straight on this machine as a managed service (launchd/systemd/schtasks). Native needs no
12
+ // Docker install, sees the host filesystem directly, and is the only mode where the bot can
13
+ // drive apps on the desktop — at the cost of the container's isolation.
11
14
  const MODE_OPTIONS = [
12
15
  { id: 'docker', title: 'Docker', subtitle: 'Isolated containers, safest default', icon: `${SVG_CDN}/docker/default.svg`, badge: 'Recommended' },
16
+ { id: 'native', title: 'Native', subtitle: 'Runs on this machine, controls apps', icon: `${SVG_CDN}/gnubash/default.svg`, badge: 'Desktop' },
13
17
  ];
14
18
  const BOT_CHANNELS = [
15
19
  { id: 'telegram', title: 'Telegram', subtitle: 'Bot API', icon: `${SVG_CDN}/telegram/default.svg`, badge: 'Tele' },
@@ -105,7 +109,8 @@ function actionIcon(name) {
105
109
  download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
106
110
  save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>',
107
111
  edit: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/>',
108
- spark: '<path d="M12 2l1.8 5.2L19 9l-5.2 1.8L12 16l-1.8-5.2L5 9l5.2-1.8Z"/>'
112
+ spark: '<path d="M12 2l1.8 5.2L19 9l-5.2 1.8L12 16l-1.8-5.2L5 9l5.2-1.8Z"/>',
113
+ key: '<circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/>'
109
114
  }[name];
110
115
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`;
111
116
  }
@@ -212,6 +217,7 @@ function installModal() {
212
217
  const osChoices = OS_OPTIONS.map(o => [o.id, t(o.title, o.title), trChoice(o).subtitle]);
213
218
  const modeChoices = [
214
219
  ['docker', 'Docker', t('\u0043ontainer c\u00f4 l\u1eadp, an to\u00e0n nh\u1ea5t', 'Isolated containers, safest default')],
220
+ ['native', 'Native', t('Ch\u1ea1y th\u1eb3ng tr\u00ean m\u00e1y n\u00e0y \u2014 kh\u00f4ng c\u1ea7n Docker, \u0111i\u1ec1u khi\u1ec3n \u0111\u01b0\u1ee3c app', 'Runs on this machine \u2014 no Docker, can drive desktop apps')],
215
221
  ];
216
222
  return `<div class="modal-backdrop install-backdrop" data-install-modal="close">
217
223
  <section class="donate-modal install-modal" role="dialog" aria-modal="true" aria-label="${t('T\u1ea1o Project','Create Project')}" onclick="event.stopPropagation()">
@@ -308,6 +314,52 @@ function openPathModal({ title, message, value = '', placeholder = '', field2 =
308
314
  render();
309
315
  setTimeout(() => document.getElementById('path-modal-input')?.focus(), 0);
310
316
  }
317
+ /**
318
+ * Shown right after PC control is granted on a native install: what the bot can now do, and the
319
+ * one thing only the operator can do — grant the OS screen permissions from the system settings.
320
+ */
321
+ function openComputerUseModal(r = {}) {
322
+ const codex = r.codex || {};
323
+ const appOk = codex.app && codex.app.present;
324
+ const cli = (r.commands || []).includes('codex');
325
+ const line = (icon, text) => `<li><span aria-hidden="true">${icon}</span><div>${text}</div></li>`;
326
+ const statusItems = [
327
+ cli
328
+ ? line('✅', t('Bot giao được việc cho <code>codex</code> — Codex tự nhìn màn hình, click, gõ phím rồi trả kết quả về.', 'The bot can hand jobs to <code>codex</code> — Codex looks at the screen, clicks and types, then reports back.'))
329
+ : line('⚠️', t('Không tìm thấy CLI <code>codex</code> — cài app ChatGPT/Codex rồi bật lại quyền này.', 'No <code>codex</code> CLI found — install the ChatGPT/Codex app, then re-enable this.')),
330
+ codex.pluginInstalled
331
+ ? line('✅', t(`Computer Use trong app Codex đã ${codex.installedNow ? 'được cài' : 'sẵn sàng'}${codex.mcpRepaired ? ' (đã sửa khai báo MCP cũ)' : ''}.`, `Computer Use in the Codex app is ${codex.installedNow ? 'installed' : 'ready'}${codex.mcpRepaired ? ' (stale MCP entry repaired)' : ''}.`))
332
+ : line('⚠️', t(`Chưa bật được Computer Use trong app Codex: ${escapeHtml(codex.error || 'không rõ lý do')}`, `Could not enable Computer Use in the Codex app: ${escapeHtml(codex.error || 'unknown reason')}`)),
333
+ appOk
334
+ ? line('✅', t('Đã thấy ứng dụng ChatGPT/Codex — <b>nhớ để app đang chạy</b>.', 'Found the ChatGPT/Codex desktop app — <b>keep it running</b>.'))
335
+ : line('⚠️', t('CHƯA thấy ứng dụng ChatGPT/Codex — cài rồi mở lên, không có nó thì không điều khiển GUI được.', 'No ChatGPT/Codex desktop app found — install and open it, GUI control needs it.')),
336
+ (r.granted && r.granted.length)
337
+ ? line('✅', t(`Đã cấp quyền chạy: ${escapeHtml(r.granted.join(', '))}.`, `Granted: ${escapeHtml(r.granted.join(', '))}.`))
338
+ : '',
339
+ ].filter(Boolean).join('');
340
+ state.confirmModal = {
341
+ icon: '🖥️',
342
+ eyebrow: t('Điều khiển máy','PC control'),
343
+ title: t('Xong — còn 1 bước bạn tự làm','Done — one step left for you'),
344
+ message: t('Model chính của bot vẫn là smart-route, không đổi.','Your bot keeps smart-route as its primary model.'),
345
+ bodyHtml: `
346
+ <ul class="cu-status">${statusItems}</ul>
347
+ <h4>${t('Cấp quyền màn hình cho máy','Grant the OS screen permissions')}</h4>
348
+ <p>${t('macOS chỉ cấp <b>Screen Recording</b> / <b>Accessibility</b> từ System Settings — bấm nút dưới, rồi bật cho <code>node</code> và app Codex.','macOS only grants <b>Screen Recording</b> / <b>Accessibility</b> from System Settings — click below, then tick <code>node</code> and the Codex app.')}</p>
349
+ <p class="cu-perm-row">
350
+ <button class="secondary cu-btn" type="button" data-host-perm="screen">${t('Chụp/quay màn hình','Screen recording')}</button>
351
+ <button class="secondary cu-btn" type="button" data-host-perm="accessibility">${t('Accessibility (chuột/bàn phím)','Accessibility (mouse/keys)')}</button>
352
+ </p>
353
+ <p class="cu-note">${t('Cách dùng: nhắn bot bình thường, ví dụ “mở TeamViewer và đọc giúp mật khẩu trên màn hình” — bot tự giao cho Codex rồi báo kết quả về.','How to use it: just ask your bot normally, e.g. “open TeamViewer and read the password on screen” — it hands the job to Codex and reports back.')}</p>
354
+ <p class="cu-note">${t('Việc giao cho Codex chạy bằng gói ChatGPT đã đăng nhập (tốn quota gói đó); chat thường vẫn đi qua các model free của <code>smart-route</code>. Điều khiển chuột/bàn phím hiện chỉ có trên macOS.','Jobs handed to Codex run on the signed-in ChatGPT plan (they spend that quota); ordinary chat still uses the free <code>smart-route</code> models. Mouse/keyboard control is macOS-only for now.')}</p>
355
+ `,
356
+ okText: t('Đã hiểu','Got it'),
357
+ okDanger: false,
358
+ hideCancel: true,
359
+ onConfirm: () => { state.confirmModal = null; render(); },
360
+ };
361
+ render();
362
+ }
311
363
  async function pickFolderPathShared() {
312
364
  try {
313
365
  const picked = await api('/api/project/pick-folder', { method: 'POST', body: {} });
@@ -816,6 +868,9 @@ function setupView() {
816
868
  function botView() {
817
869
  const s = state.install || {};
818
870
  const sys = state.system || {};
871
+ // Native projects have no container: Rebuild (no image) and Grant disk (host FS is already
872
+ // reachable) do not apply, so those buttons are hidden rather than failing when pressed.
873
+ const isNativeMode = s.deployMode === 'native';
819
874
  const bots = s.bots || [];
820
875
  const ch = state.botChannel || 'telegram';
821
876
  const channelBots = bots.filter(b => b.channel === ch);
@@ -870,10 +925,12 @@ function botView() {
870
925
  <div class="runtime-status-card"><div class="runtime-status-head"><span>9Router</span>${statusBadge(s.routerStatus)}</div><div class="runtime-card-actions"><a class="runtime-open-btn secondary icon-btn2" href="${sameHostUrl(s.routerUrl, 20128)}" target="_blank" rel="noopener" style="justify-content:center; flex:1; font-size:12px; height:36px; border-width:1px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>${t('Mở web','Open')}</a><button class="runtime-open-btn icon-btn2" data-update-router type="button" style="justify-content:center; flex:1; font-size:12px; height:36px; border:none; background:rgba(255,36,54,.15); color:#ff4b5d;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px; height:14px;"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>${t('Update','Update')}</button></div></div>
871
926
  </div>
872
927
 
928
+ ${''/* native: no container → no image to rebuild, and the host FS is already visible */}
873
929
  <div class="bot-docker-actions" style="display:flex; gap:8px; margin-top:14px;">
874
930
  <button class="secondary icon-btn2" data-bot-restart type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="${t('Khởi động lại container bot','Restart bot container')}">🔄 ${t('Restart','Restart')}</button>
875
- <button class="secondary icon-btn2" data-bot-rebuild type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="docker compose up -d --build">🔨 ${t('Rebuild','Rebuild')}</button>
876
- <button class="secondary icon-btn2" data-bot-add-mount type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="${t('Cấp quyền ổ đĩa/thư mục cho bot','Grant the bot a disk/folder')}">💽 ${t('Cấp quyền ổ đĩa','Grant disk')}</button>
931
+ ${isNativeMode ? '' : `<button class="secondary icon-btn2" data-bot-rebuild type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="docker compose up -d --build">🔨 ${t('Rebuild','Rebuild')}</button>`}
932
+ ${isNativeMode ? '' : `<button class="secondary icon-btn2" data-bot-add-mount type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="${t('Cấp quyền ổ đĩa/thư mục cho bot','Grant the bot a disk/folder')}">💽 ${t('Cấp quyền ổ đĩa','Grant disk')}</button>`}
933
+ <button class="secondary icon-btn2" data-bot-host-control type="button" style="flex:1; justify-content:center; font-size:12px; height:34px; border-width:1px;" title="${t('Cho bot mở Chrome/ứng dụng trên máy này','Let the bot open Chrome/apps on this machine')}">🖥️ ${t('Điều khiển máy','Control PC')}</button>
877
934
  </div>
878
935
 
879
936
  <div class="dash-version-list" style="margin-top: 18px;">
@@ -1097,7 +1154,13 @@ function botSkillsPanel() {
1097
1154
  if (isInstalled) {
1098
1155
  toggleHtml = `<label class="feature-switch" title="${locked ? t('Bắt buộc cho bot Zalo — không thể tắt','Required for Zalo bots — cannot be disabled') : ''}"${locked ? ' style="opacity:.4;pointer-events:none;cursor:not-allowed;"' : ''}><input type="checkbox" data-feature-toggle="${key}" ${(flags[key] || locked) ? 'checked' : ''} ${(loading || locked) ? 'disabled' : ''}/><span></span></label>`;
1099
1156
  if (item.openWebPort) {
1100
- secs.push(`<a class="secondary icon-btn2" href="${sameHostUrl('', item.openWebPort)}${item.openWebPath || ''}" target="_blank" rel="noopener" title="${t('Mở dashboard của plugin','Open the plugin dashboard')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px;">${actionIcon('link')}<span>${t('Mở web','Open')}</span></a>`);
1157
+ // zalo-mod's dashboard is always gateway port + 1 (docker 18790, native 18890, or whatever
1158
+ // the SELECTED project's gateway runs on) — never the hardcoded default. Derive it from the
1159
+ // active project's gatewayUrl so every project opens its own dashboard.
1160
+ let gwPort = 0;
1161
+ try { gwPort = parseInt(new URL(state.install?.gatewayUrl).port, 10) || 0; } catch {}
1162
+ const openPort = (gwPort ? gwPort + 1 : item.openWebPort);
1163
+ secs.push(`<a class="secondary icon-btn2" href="${sameHostUrl('', openPort)}${item.openWebPath || ''}" target="_blank" rel="noopener" title="${t('Mở dashboard của plugin','Open the plugin dashboard')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px;">${actionIcon('link')}<span>${t('Mở web','Open')}</span></a>`);
1101
1164
  }
1102
1165
  if (requiresInstall) {
1103
1166
  secs.push(`<button class="secondary icon-btn2 update-plugin-btn" type="button" data-feature-install="${key}" ${loading ? 'disabled' : ''} title="${t('Cập nhật lên bản mới nhất','Update to latest version')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px; color:#ffb020; border-color: rgba(255,176,32,0.25); background: rgba(255,176,32,0.05);">${actionIcon('refresh')}<span>${t('Cập nhật','Update')}</span></button>`);
@@ -1176,6 +1239,21 @@ function wireTab() {
1176
1239
  if (action === 'cancel' || !m) { state.confirmModal = null; render(); return; }
1177
1240
  if (action === 'ok' && typeof m.onConfirm === 'function') await m.onConfirm();
1178
1241
  }));
1242
+ // Screen-recording / accessibility grants: only the OS can give these, so the button opens the
1243
+ // right settings pane (and on macOS pokes the capture API so the system prompt shows up).
1244
+ document.querySelectorAll('[data-host-perm]').forEach(btn => btn.onclick = () => withButtonLoading(btn, async () => {
1245
+ const kind = btn.dataset.hostPerm;
1246
+ try {
1247
+ const r = await api('/api/host/permissions', { method: 'POST', body: { kind, projectDir: activeProjectDir() } });
1248
+ if (!r.opened) return showToast(t('Không mở được','Could not open'), t('Hệ điều hành này không có bảng cài đặt đó — cấp quyền thủ công.','This OS has no such settings pane — grant it manually.'), 'error');
1249
+ const granted = r.screen && r.screen.supported ? r.screen.granted : null;
1250
+ showToast(t('Đã mở cài đặt quyền','Opened settings'),
1251
+ granted === true ? t('Quyền chụp/quay màn hình: đã có. Bật thêm Accessibility nếu cần gõ/click.','Screen recording: already granted. Also enable Accessibility for typing/clicking.')
1252
+ : granted === false ? t('Chưa có quyền — bật cho "node" (và app Codex) trong danh sách vừa mở, rồi restart bot.','Not granted yet — tick "node" (and the Codex app) in the list that just opened, then restart the bot.')
1253
+ : t('Bật quyền cho "node" và app Codex trong danh sách vừa mở.','Tick "node" and the Codex app in the list that just opened.'),
1254
+ granted === false ? 'error' : 'success', 8000);
1255
+ } catch (err) { showToast(t('Thất bại','Failed'), err.message, 'error'); }
1256
+ }));
1179
1257
  document.querySelectorAll('[data-pref]').forEach(btn => btn.onclick = () => {
1180
1258
  state[btn.dataset.pref] = btn.dataset.value;
1181
1259
  localStorage.setItem('openclaw-'+btn.dataset.pref, btn.dataset.value);
@@ -1320,6 +1398,55 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1320
1398
  } catch (err) { showToast(t('Thất bại','Failed'), err.message, 'error'); }
1321
1399
  },
1322
1400
  }));
1401
+ document.querySelectorAll('[data-bot-host-control]').forEach(btn => btn.onclick = async () => {
1402
+ let cur;
1403
+ try { cur = await api('/api/host/control' + projectQuery({})); }
1404
+ catch (err) { return showToast(t('Thất bại','Failed'), err.message, 'error'); }
1405
+ const on = !!cur.enabled;
1406
+ const apps = cur.apps || [];
1407
+ const grants = cur.grants || [];
1408
+ const chips = (items) => items.map((i) => `<code>${escapeHtml(i)}</code>`).join(' ');
1409
+ const scripts = grants.filter((g) => ['node', 'npx', 'codex'].includes(g));
1410
+ // One row per capability instead of a wall of prose — the operator is granting real access to
1411
+ // their machine and should be able to see, at a glance, exactly what each row means.
1412
+ const rows = [
1413
+ { icon: '🗂', title: t('Mở ứng dụng','Open apps'), desc: apps.length ? chips(apps) : t('chưa dò được app nào','no apps detected') },
1414
+ ...(cur.native ? [
1415
+ { icon: '📸', title: t('Chụp & quay màn hình','Screen capture & recording'), desc: t('bot nhìn được màn hình khi bạn nhờ','the bot can see your screen when you ask') },
1416
+ { icon: '🖱', title: t('Điều khiển chuột/bàn phím','Mouse & keyboard control'), desc: t('bot giao việc cho Codex CLI (dùng gói ChatGPT đã đăng nhập); macOS','the bot hands the job to the Codex CLI on your ChatGPT plan; macOS only') },
1417
+ { icon: '⚙️', title: t('Chạy script','Run scripts'), desc: `${chips(scripts.length ? scripts : ['node'])} — <b>${t('chạy được mã tuỳ ý trên máy này','arbitrary code on this machine')}</b>` },
1418
+ ] : []),
1419
+ ];
1420
+ const bodyHtml = on ? '' : `
1421
+ <ul class="cu-status grant-list">${rows.map((r) => `<li><span aria-hidden="true">${r.icon}</span><div><b>${r.title}</b><br>${r.desc}</div></li>`).join('')}</ul>
1422
+ <p class="cu-note">${t('Danh sách sửa trong <code>.openclaw/host-control.json</code>. Chỉ dùng trên máy có màn hình (không áp dụng VPS headless).','Edit the list in <code>.openclaw/host-control.json</code>. Desktop only (not a headless VPS).')}</p>`;
1423
+ state.confirmModal = {
1424
+ icon: '🖥️',
1425
+ eyebrow: t('Điều khiển máy','PC control'),
1426
+ title: on ? t('Tắt điều khiển máy?','Turn off PC control?') : t('Bật điều khiển máy?','Turn on PC control?'),
1427
+ message: on
1428
+ ? t('Bot sẽ không còn mở được Chrome/ứng dụng trên máy này nữa.','The bot will no longer be able to open Chrome/apps on this machine.')
1429
+ : t('Bạn sắp cho bot những quyền sau trên MÁY NÀY:','You are about to grant the bot the following on THIS machine:'),
1430
+ bodyHtml,
1431
+ okText: on ? t('Tắt','Turn off') : t('Bật','Turn on'),
1432
+ okDanger: on,
1433
+ onConfirm: async () => {
1434
+ state.confirmModal = null; render();
1435
+ if (!on) showToast(t('Đang bật','Enabling'), t('Đang cấp quyền & cài Computer Use (có thể mất ~30s)…','Granting access & installing Computer Use (may take ~30s)…'), 'success');
1436
+ try {
1437
+ const r = await api('/api/host/control', { method: 'POST', body: { enabled: !on, projectDir: activeProjectDir() } });
1438
+ if (!on && r.started && r.started.ok === false && r.started.reason) {
1439
+ showToast(t('Đã bật (lưu ý)','Enabled (note)'), t('Không khởi động được service: ','Service did not start: ') + r.started.reason, 'error');
1440
+ } else {
1441
+ showToast(r.enabled ? t('Đã bật điều khiển máy','PC control on') : t('Đã tắt','Turned off'),
1442
+ r.enabled ? t('Bot có thể mở Chrome/app trong danh sách.','The bot can open allow-listed Chrome/apps.') : t('Đã thu hồi quyền.','Access revoked.'), 'success');
1443
+ }
1444
+ if (r.enabled && r.native) openComputerUseModal(r);
1445
+ } catch (err) { showToast(t('Thất bại','Failed'), err.message, 'error'); }
1446
+ },
1447
+ };
1448
+ render();
1449
+ });
1323
1450
  document.querySelectorAll('[data-project-remove]').forEach(btn => btn.onclick = (ev) => {
1324
1451
  ev.stopPropagation();
1325
1452
  const projectDir = btn.dataset.projectRemove;
@@ -1593,7 +1720,7 @@ function zaloToolbar(channelBots = []) {
1593
1720
  const active = channelBots.find((bot) => bot.id === state.activeBotId) || channelBots[0];
1594
1721
  const health = zaloAccountHealth(active);
1595
1722
  const loginLabel = health?.sessionSaved ? t('Đăng nhập lại','Log in again') : t('Đăng nhập Zalo','Zalo Login');
1596
- return `<div class="zalo-toolbar"><button class="secondary btn-inline" data-zalo-health-refresh type="button">${actionIcon('refresh')}<span>${t('Làm mới','Refresh')}</span></button><button class="secondary btn-inline" data-zalo-login-trigger type="button">🔑 <span>${loginLabel}</span></button></div>`;
1723
+ return `<div class="zalo-toolbar"><button class="secondary btn-inline" data-zalo-health-refresh type="button">${actionIcon('refresh')}<span>${t('Làm mới','Refresh')}</span></button><button class="secondary btn-inline" data-zalo-login-trigger type="button">${actionIcon('key')}<span>${loginLabel}</span></button></div>`;
1597
1724
  }
1598
1725
  async function loadZaloHealth(silent=false){
1599
1726
  if (!activeProjectDir()) { state.zaloHealth = null; return; }
@@ -1812,7 +1812,7 @@ html[data-theme="light"] .bottom .nav.active {
1812
1812
  }
1813
1813
 
1814
1814
  /* Compact per-account Zalo health — shared versions stay in the right status column. */
1815
- .zalo-toolbar{display:flex;gap:8px;align-items:center}.zalo-toolbar .btn-inline{min-height:36px;padding:6px 12px;font-size:13px}.zalo-toolbar svg{width:14px;height:14px}.bot-item-title{display:flex;align-items:center;gap:8px;min-width:0;padding-right:58px}.bot-item-title b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.zalo-quick-badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:999px;border:1px solid rgba(24,194,156,.35);background:rgba(24,194,156,.12);color:#25d6ad;font-size:10px;font-weight:800;white-space:nowrap}.zalo-bot-health{display:grid;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid rgba(255,255,255,.07)}.zalo-bot-health>div{display:flex;align-items:center;justify-content:space-between;gap:10px}.zalo-bot-health span{color:var(--muted);font-size:11px;font-weight:700}.zalo-bot-health em{font-size:11px;font-style:normal;font-weight:800;text-align:right}.zalo-bot-health em.ok{color:#25d6ad}.zalo-bot-health em.warn{color:#f7bf48}.zalo-bot-health em.bad{color:#ff6574}html[data-theme="light"] .zalo-bot-health{border-top-color:rgba(15,23,42,.08)}@media(max-width:640px){.zalo-toolbar{display:grid;grid-template-columns:1fr 1fr;width:100%}.zalo-toolbar .btn-inline{justify-content:center;padding:6px 8px}.bot-item-title{padding-right:54px}}
1815
+ .zalo-toolbar{display:flex;gap:8px;align-items:center}.zalo-toolbar .btn-inline{min-height:36px;padding:6px 12px;font-size:13px;gap:7px}.zalo-toolbar svg{width:14px;height:14px;flex:0 0 auto}.bot-item-title{display:flex;align-items:center;gap:8px;min-width:0;padding-right:58px}.bot-item-title b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.zalo-quick-badge{display:inline-flex;align-items:center;padding:2px 7px;border-radius:999px;border:1px solid rgba(24,194,156,.35);background:rgba(24,194,156,.12);color:#25d6ad;font-size:10px;font-weight:800;white-space:nowrap}.zalo-bot-health{display:grid;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid rgba(255,255,255,.07)}.zalo-bot-health>div{display:flex;align-items:center;justify-content:space-between;gap:10px}.zalo-bot-health span{color:var(--muted);font-size:11px;font-weight:700}.zalo-bot-health em{font-size:11px;font-style:normal;font-weight:800;text-align:right}.zalo-bot-health em.ok{color:#25d6ad}.zalo-bot-health em.warn{color:#f7bf48}.zalo-bot-health em.bad{color:#ff6574}html[data-theme="light"] .zalo-bot-health{border-top-color:rgba(15,23,42,.08)}@media(max-width:640px){.zalo-toolbar{display:grid;grid-template-columns:1fr 1fr;width:100%}.zalo-toolbar .btn-inline{justify-content:center;padding:6px 8px}.bot-item-title{padding-right:54px}}
1816
1816
 
1817
1817
  /* Ensure bottom navigation bar is visible on tablet and mobile (under 1180px) */
1818
1818
  @media (max-width: 1180px) {
@@ -1960,6 +1960,25 @@ body:has(.modal-backdrop) .bottom{display:none!important}
1960
1960
  .confirm-body .cmd{margin-top:0;user-select:all;word-break:break-all;white-space:pre-wrap;font-size:11.5px;line-height:1.5}
1961
1961
  .guide-step{margin:4px 0 0;color:var(--body);font-size:13px;font-weight:700}
1962
1962
 
1963
+ /* [Computer Use modal] Post-grant checklist: what got provisioned + the two manual steps
1964
+ (9router combo, OS screen permissions). Scrolls on short screens instead of clipping. */
1965
+ .confirm-body{max-height:min(58vh,520px);overflow-y:auto}
1966
+ .confirm-body h4{margin:10px 0 2px;font-size:13px;font-weight:800;color:var(--body)}
1967
+ .confirm-body p{margin:0;font-size:12.5px;line-height:1.6;color:var(--muted)}
1968
+ .confirm-body code{padding:1px 5px;border-radius:5px;background:rgba(127,127,127,.16);font-size:11.5px}
1969
+ .cu-status{margin:0;padding:0;list-style:none;display:grid;gap:6px}
1970
+ .cu-status li{display:flex;gap:8px;align-items:flex-start;font-size:12.5px;line-height:1.55;color:var(--body)}
1971
+ .cu-status li>span{flex:0 0 auto;line-height:1.5}
1972
+ .cu-status li>div{min-width:0}
1973
+ /* Grant checklist in the confirm step: boxed rows so each capability reads as its own item. */
1974
+ .grant-list li{gap:10px;padding:9px 11px;border-radius:10px;background:rgba(127,127,127,.10)}
1975
+ .grant-list b{font-weight:800}
1976
+ .grant-list li>div{color:var(--muted)}
1977
+ .cu-models{display:flex;flex-wrap:wrap;gap:6px}
1978
+ .cu-perm-row{display:flex;flex-wrap:wrap;gap:8px}
1979
+ .cu-btn{display:inline-flex;align-items:center;justify-content:center;height:32px;padding:0 12px;border-radius:9px;font-size:12px;font-weight:700;text-decoration:none;cursor:pointer}
1980
+ .cu-note{margin-top:6px!important;padding:8px 10px;border-radius:9px;background:rgba(127,127,127,.12);font-size:11.5px!important}
1981
+
1963
1982
  @media(max-width:760px){
1964
1983
  /* [Topbar] Keep search + theme + language on ONE row; theme becomes icon-only and the
1965
1984
  language globe icon is dropped so the whole bar stays compact. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.14.0",
3
+ "version": "5.15.0",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {