create-openclaw-bot 5.17.1 → 5.17.2

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/web/app.js CHANGED
@@ -10,7 +10,7 @@ const OS_OPTIONS = [
10
10
  // Two deploy modes. Docker isolates the bot in a container; native runs openclaw + 9router
11
11
  // straight on this machine as a managed service (launchd/systemd/schtasks). Native needs no
12
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.
13
+ // drive apps on the desktop - at the cost of the container's isolation.
14
14
  const MODE_OPTIONS = [
15
15
  { id: 'docker', title: 'Docker', subtitle: 'Isolated containers, safest default', icon: `${SVG_CDN}/docker/default.svg`, badge: 'Recommended' },
16
16
  { id: 'native', title: 'Native', subtitle: 'Runs on this machine, controls apps', icon: `${SVG_CDN}/gnubash/default.svg`, badge: 'Desktop' },
@@ -177,7 +177,7 @@ function confirmModal() {
177
177
  <button class="modal-x" data-confirm-action="cancel" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
178
178
  <div class="donate-head"><span aria-hidden="true">${escapeHtml(m.icon || '⚠')}</span><div><p>${escapeHtml(m.eyebrow || t('Xác nhận','Confirm'))}</p><h2>${escapeHtml(m.title)}</h2><small>${escapeHtml(m.message || '')}</small></div></div>
179
179
  ${m.bodyHtml ? `<div class="confirm-body">${m.bodyHtml}</div>` : ''}
180
- <div class="confirm-actions">${m.hideCancel ? '' : `<button class="secondary" data-confirm-action="cancel">${t('Hủy','Cancel')}</button>`}<button class="primary ${m.okDanger === false ? '' : 'danger'}" data-confirm-action="ok">${escapeHtml(m.okText || t('Xóa','Delete'))}</button></div>
180
+ <div class="confirm-actions">${m.hideCancel ? '' : `<button class="secondary" data-confirm-action="cancel">${escapeHtml(m.cancelText || t('Hủy','Cancel'))}</button>`}<button class="primary ${m.okDanger === false ? '' : 'danger'}" data-confirm-action="ok">${escapeHtml(m.okText || t('Xóa','Delete'))}</button></div>
181
181
  </section>
182
182
  </div>`;
183
183
  }
@@ -321,7 +321,7 @@ function openPathModal({ title, message, value = '', placeholder = '', field2 =
321
321
  }
322
322
  /**
323
323
  * Shown right after PC control is granted on a native install: what the bot can now do, and the
324
- * one thing only the operator can do grant the OS screen permissions from the system settings.
324
+ * one thing only the operator can do - grant the OS screen permissions from the system settings.
325
325
  */
326
326
  function openComputerUseModal(r = {}) {
327
327
  const cu = r.computerUse || {};
@@ -362,7 +362,7 @@ function openComputerUseModal(r = {}) {
362
362
  ${permBlock}
363
363
  <p class="cu-note">${t('Cách dùng: nhắn bot bình thường, ví dụ “chụp màn hình cho anh xem” hoặc “mở TeamViewer rồi đọc mật khẩu trên màn hình”.','How to use it: just ask your bot normally, e.g. "take a screenshot" or "open TeamViewer and read the password on screen".')}</p>
364
364
  <p class="cu-note">${t('Chỉ dùng được trên máy có màn hình. Máy chủ VPS không màn hình thì không áp dụng. Tắt nút này là thu hồi lại toàn bộ quyền trên.','Only works on a machine with a screen. A headless VPS cannot use this. Turning the switch off revokes all of it.')}</p>
365
- <p class="cu-note">${t('<b>Sau khi khởi động lại máy hoặc đăng xuất Windows, phần điều khiển máy sẽ tắt.</b> Mở lại giao diện này và bật lại nút xong. Bot vẫn chat bình thường, chỉ riêng phần nhìn và điều khiển màn hình cần bật lại.','<b>Restarting or signing out of Windows turns computer control off.</b> Open this screen and switch it back on. Chat keeps working meanwhile; only seeing and driving the screen needs re-enabling.')}</p>
365
+ <p class="cu-note">${t('Sau khi khởi động lại máy, bấm <b>"1 - KHOI DONG BOT"</b> là phần điều khiển máy cũng tự bật lại cùng bot.','After restarting the machine, pressing <b>"1 - KHOI DONG BOT"</b> brings computer control back up along with the bot.')}</p>
366
366
  `,
367
367
  okText: t('Đã hiểu','Got it'),
368
368
  okDanger: false,
@@ -371,6 +371,78 @@ function openComputerUseModal(r = {}) {
371
371
  };
372
372
  }
373
373
 
374
+ /**
375
+ * Offer the move off Docker, once, when we can see the project is still on it.
376
+ *
377
+ * Relying on the operator to press "Update" does not work: nothing on screen says that Update is
378
+ * ALSO the migration, so a machine sits on Docker indefinitely. And it cannot sit there safely -
379
+ * OpenClaw 2026.9 cannot write its own settings through a Docker shared folder on Windows, so the
380
+ * next image rebuild takes the bot down for good. Say that plainly, show what is gained, and make
381
+ * the move one button.
382
+ *
383
+ * Asked once per project, then remembered: a prompt that returns on every page load is a prompt
384
+ * people learn to dismiss without reading.
385
+ */
386
+ function maybeOfferNativeMigration() {
387
+ const s = state.install || {};
388
+ if (s.deployMode !== 'docker') return;
389
+ const dir = s.projectDir || '';
390
+ if (!dir) return;
391
+ const key = 'openclaw-native-offer:' + dir;
392
+ try { if (localStorage.getItem(key)) return; } catch (_) { /* private window: just ask */ }
393
+ if (state.confirmModal) return; // never stack on top of another dialog
394
+ const rows = [
395
+ ['🖥️', t('Bot điều khiển được máy này', 'Your bot can drive this machine'),
396
+ t('Chụp màn hình, rê chuột, bấm, gõ phím. Bản Docker không làm được vì bot nằm trong container, không thấy màn hình.',
397
+ 'Screenshots, mouse, clicks, typing. Docker cannot do this: the bot sits in a container and never sees your screen.')],
398
+ ['🛡️', t('Tránh được lỗi làm chết bot', 'Avoids the fault that kills bots'),
399
+ t('OpenClaw bản mới không ghi được cấu hình qua thư mục chia sẻ của Docker trên Windows. Cứ dựng lại image một lần là bot chết hẳn.',
400
+ 'Recent OpenClaw cannot write its settings through a Docker shared folder on Windows. One image rebuild and the bot is gone.')],
401
+ ['⚡', t('Nhẹ và nhanh hơn', 'Lighter and faster'),
402
+ t('Không cần Docker Desktop chạy nền, bot khởi động nhanh hơn và ăn ít RAM hơn.',
403
+ 'No Docker Desktop running in the background, faster start-up and less RAM.')],
404
+ ['🔒', t('An toàn như cũ', 'Just as safe'),
405
+ t('Bot vẫn chỉ nghe trên máy này, không mở ra ngoài internet. Quyền điều khiển máy vẫn mặc định TẮT.',
406
+ 'The bot still listens on this machine only, never the open internet. Computer control stays OFF by default.')],
407
+ ];
408
+ state.confirmModal = {
409
+ icon: '📦',
410
+ eyebrow: t('Chuyển sang chạy thẳng trên máy', 'Move off Docker'),
411
+ title: t('Bot đang chạy trong Docker', 'This bot still runs in Docker'),
412
+ message: t('Chuyển sang chạy thẳng trên máy được không? Toàn bộ bot, tệp trong thư mục làm việc và phiên đăng nhập Zalo giữ nguyên.',
413
+ 'Move it to run directly on this machine? Every bot, every workspace file and every Zalo login comes with it.'),
414
+ bodyHtml: `
415
+ <ul class="cu-status grant-list">${rows.map(([i, ttl, d]) =>
416
+ `<li><span aria-hidden="true">${i}</span><div><b>${ttl}</b><br>${d}</div></li>`).join('')}</ul>
417
+ <p class="cu-note">${t('Mất khoảng 2-5 phút, bot ngừng trả lời trong lúc đó. Container cũ chỉ được <b>dừng</b> chứ không xoá, nên vẫn có đường quay lui.',
418
+ 'Takes about 2-5 minutes, during which the bot stops replying. The old containers are <b>stopped</b>, not deleted, so there is a way back.')}</p>`,
419
+ okText: t('Chuyển ngay', 'Move now'),
420
+ cancelText: t('Để sau', 'Later'),
421
+ onCancel: () => {
422
+ // "Later" means later, not never - but stop asking on this machine.
423
+ try { localStorage.setItem(key, String(Date.now())); } catch (_) {}
424
+ state.confirmModal = null;
425
+ render();
426
+ },
427
+ onConfirm: async () => {
428
+ try { localStorage.setItem(key, String(Date.now())); } catch (_) {}
429
+ state.confirmModal = null;
430
+ render();
431
+ showToast(t('Đang chuyển sang chạy thẳng trên máy', 'Moving off Docker'),
432
+ t('Giữ cửa sổ này mở. Xem tiến trình ở tab Nhật ký.', 'Keep this window open. Watch the Log tab for progress.'));
433
+ try {
434
+ await api('/api/runtime/update', { method: 'POST', body: { projectDir: dir, target: 'openclaw' } });
435
+ await loadStatus(true);
436
+ showToast(t('Đã chuyển xong', 'Moved'), t('Bot giờ chạy thẳng trên máy này.', 'The bot now runs directly on this machine.'));
437
+ } catch (err) {
438
+ showToast(t('Chuyển không xong', 'Move failed'), err.message, 'error');
439
+ }
440
+ render();
441
+ },
442
+ };
443
+ render();
444
+ }
445
+
374
446
  async function pickFolderPathShared() {
375
447
  try {
376
448
  const picked = await api('/api/project/pick-folder', { method: 'POST', body: {} });
@@ -404,7 +476,7 @@ function isVersionNewer(a, b) {
404
476
  }
405
477
 
406
478
  // Build a URL on the SAME host the dashboard is currently viewed from, so "Open" works
407
- // whether you reached it via a localhost tunnel, the server's IP, or a domain and never
479
+ // whether you reached it via a localhost tunnel, the server's IP, or a domain - and never
408
480
  // points at the viewer's own localhost when browsing from another machine. Port comes from
409
481
  // the backend-provided URL (which carries the right port) or the fallback.
410
482
  function sameHostUrl(rawUrl, fallbackPort) {
@@ -415,7 +487,7 @@ function sameHostUrl(rawUrl, fallbackPort) {
415
487
  return `${proto}//${host}:${port}`;
416
488
  }
417
489
 
418
- // "Open from another machine" helper builds a ready SSH-tunnel command (auto-filled
490
+ // "Open from another machine" helper - builds a ready SSH-tunnel command (auto-filled
419
491
  // with the server's public IP + the dashboard/gateway/9router/zalo-mod ports) so any user
420
492
  // on a VPS can reach the web UIs from their own computer without knowing how to set it up.
421
493
  function remoteAccessPanel(s = {}) {
@@ -424,7 +496,7 @@ function remoteAccessPanel(s = {}) {
424
496
  const host = r.host || '<your-server-ip>';
425
497
  const user = r.user || 'root';
426
498
  const portOf = (raw, def) => { try { return new URL(raw).port || def; } catch { return def; } };
427
- // zalo-mod's dashboard is gateway port + 1, not a fixed 18790 the same rule the plugin card's
499
+ // zalo-mod's dashboard is gateway port + 1, not a fixed 18790 - the same rule the plugin card's
428
500
  // "Open" button uses. Hardcoding it meant any project whose gateway is not on 18789 got a tunnel
429
501
  // command missing the dashboard port, and the dashboard then simply failed to load with no clue why.
430
502
  const gwPort = Number(portOf(s.gatewayUrl, 18789));
@@ -432,7 +504,7 @@ function remoteAccessPanel(s = {}) {
432
504
  const cmd = `ssh ${ports.map((p) => `-L ${p}:127.0.0.1:${p}`).join(' ')} ${user}@${host}`;
433
505
  return `<details class="card" style="margin-top:12px;" ${r.headless ? 'open' : ''}>
434
506
  <summary style="cursor:pointer; font-weight:600;">🌐 ${t('Mở từ máy khác (VPS/server)', 'Open from another machine (VPS/server)')}</summary>
435
- <p class="lead" style="margin-top:8px;">${t('Server không có trình duyệt trên MÁY của bạn chạy lệnh này rồi mở', 'No browser on the server on YOUR computer run this, then open')} <code>http://localhost:${r.uiPort || 51789}</code>:</p>
507
+ <p class="lead" style="margin-top:8px;">${t('Server không có trình duyệt - trên MÁY của bạn chạy lệnh này rồi mở', 'No browser on the server - on YOUR computer run this, then open')} <code>http://localhost:${r.uiPort || 51789}</code>:</p>
436
508
  <div style="display:flex; gap:8px;">
437
509
  <input readonly value="${escapeHtml(cmd)}" onclick="this.select()" style="flex:1; font-family:ui-monospace,monospace; font-size:12px; padding:8px 10px; border-radius:8px; border:1px solid rgba(255,255,255,.15); background:rgba(255,255,255,.03); color:inherit;" />
438
510
  <button class="secondary icon-btn2" type="button" data-copy="${escapeHtml(cmd)}">${actionIcon('link')}<span>${t('Copy', 'Copy')}</span></button>
@@ -444,7 +516,7 @@ function remoteAccessPanel(s = {}) {
444
516
  function topbarActionsHtml() {
445
517
  const setupVer = state.system?.versions?.setup;
446
518
  const latestSetupVer = state.system?.versions?.latestSetup;
447
- // Only offer the update when the remote is genuinely newer (not merely different
519
+ // Only offer the update when the remote is genuinely newer (not merely different -
448
520
  // a local/dev build can be ahead of what's published).
449
521
  const hasNewVersion = !!(setupVer && latestSetupVer && isVersionNewer(latestSetupVer, setupVer));
450
522
  return `
@@ -466,7 +538,7 @@ function topbarActionsHtml() {
466
538
  <button class="seg__btn ${state.lang==='en'?'is-active':''}" data-pref="lang" data-value="en">EN</button>
467
539
  </div>
468
540
  ${hasNewVersion ? `
469
- <button class="topbar__btn seg__btn" data-update-setup title="${t('Đang dùng', 'Currently on')} ${setupVer} ${t('bấm để lên', 'click to upgrade to')} ${latestSetupVer}" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--ok); background: rgba(46, 230, 166, 0.08); color: var(--ok); font-weight: 600; cursor: pointer; transition: background 0.2s;">
541
+ <button class="topbar__btn seg__btn" data-update-setup title="${t('Đang dùng', 'Currently on')} ${setupVer} - ${t('bấm để lên', 'click to upgrade to')} ${latestSetupVer}" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--ok); background: rgba(46, 230, 166, 0.08); color: var(--ok); font-weight: 600; cursor: pointer; transition: background 0.2s;">
470
542
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.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>
471
543
  <span>${t('Cập nhật', 'Update')} ${latestSetupVer}</span>
472
544
  </button>
@@ -640,13 +712,13 @@ function wireSkillsHandlers(scope = document) {
640
712
  try {
641
713
  const r = await api('/api/browser/start-chrome', { method: 'POST', body: {} });
642
714
  if (r.headless) {
643
- // VPS has no display guide the user to run Chrome on THEIR machine + a reverse tunnel.
715
+ // VPS has no display - guide the user to run Chrome on THEIR machine + a reverse tunnel.
644
716
  state.confirmModal = {
645
717
  icon: '🌐',
646
718
  eyebrow: t('Chrome debug', 'Chrome debug'),
647
719
  title: t('Dùng Chrome trên máy của bạn', 'Use Chrome on your computer'),
648
- message: t('VPS không có màn hình nên không mở Chrome tại đây. Chạy 2 lệnh dưới trên máy của bạn và GIỮ chúng chạy bot sẽ tự dùng Chrome đó (cổng 9222). Không làm cũng không sao: bot vẫn có trình duyệt ẩn (headless) sẵn trong container.', 'This VPS has no display. Run the two commands below on YOUR computer and keep them running the bot will use that Chrome (port 9222). Optional: the bot already has a built-in headless browser.'),
649
- bodyHtml: `<p class="guide-step">1. ${t('Mở Chrome debug trên máy bạn macOS:', 'Start Chrome debug on your machine macOS:')}</p><code class="cmd">${escapeHtml(r.chromeCmdMac)}</code><p class="guide-step">${t('Hoặc Windows:', 'Or Windows:')}</p><code class="cmd">${escapeHtml(r.chromeCmdWin)}</code><p class="guide-step">2. ${t('Tạo tunnel về VPS (cửa sổ terminal khác):', 'Create the reverse tunnel (another terminal):')}</p><code class="cmd">${escapeHtml(r.tunnelCmd)}</code>`,
720
+ message: t('VPS không có màn hình nên không mở Chrome tại đây. Chạy 2 lệnh dưới trên máy của bạn và GIỮ chúng chạy - bot sẽ tự dùng Chrome đó (cổng 9222). Không làm cũng không sao: bot vẫn có trình duyệt ẩn (headless) sẵn trong container.', 'This VPS has no display. Run the two commands below on YOUR computer and keep them running - the bot will use that Chrome (port 9222). Optional: the bot already has a built-in headless browser.'),
721
+ bodyHtml: `<p class="guide-step">1. ${t('Mở Chrome debug trên máy bạn - macOS:', 'Start Chrome debug on your machine - macOS:')}</p><code class="cmd">${escapeHtml(r.chromeCmdMac)}</code><p class="guide-step">${t('Hoặc Windows:', 'Or Windows:')}</p><code class="cmd">${escapeHtml(r.chromeCmdWin)}</code><p class="guide-step">2. ${t('Tạo tunnel về VPS (cửa sổ terminal khác):', 'Create the reverse tunnel (another terminal):')}</p><code class="cmd">${escapeHtml(r.tunnelCmd)}</code>`,
650
722
  okText: t('Đã hiểu', 'Got it'),
651
723
  okDanger: false,
652
724
  hideCancel: true,
@@ -654,10 +726,10 @@ function wireSkillsHandlers(scope = document) {
654
726
  };
655
727
  render();
656
728
  } else {
657
- // The profile is a copy of the operator's own, so their normal Chrome can stay open
729
+ // The profile is a copy of the operator's own, so their normal Chrome can stay open -
658
730
  // say so, because the old flow required closing every Chrome window first.
659
731
  showToast(t('Đã mở Chrome cho bot', 'Chrome opened for the bot'),
660
- `${t('Cổng', 'Port')} ${r.port} ${t('cửa sổ Chrome mới này dùng bản sao profile của bạn (đã đăng nhập sẵn); Chrome thường của bạn cứ để nguyên.', 'the new window runs a copy of your profile (already signed in); your normal Chrome can stay open.')}`, 'success');
732
+ `${t('Cổng', 'Port')} ${r.port} - ${t('cửa sổ Chrome mới này dùng bản sao profile của bạn (đã đăng nhập sẵn); Chrome thường của bạn cứ để nguyên.', 'the new window runs a copy of your profile (already signed in); your normal Chrome can stay open.')}`, 'success');
661
733
  }
662
734
  } catch (err) {
663
735
  showToast(t('Thất bại', 'Failed'), err.message, 'error');
@@ -679,7 +751,7 @@ function content() {
679
751
  return `<div class="log-toolbar"><button class="copy-log" data-copy-log type="button" aria-label="Copy logs">${copyIcon()} ${t('Copy log','Copy log')}</button></div><div class="terminal big">${state.logs.map(l=>`<p>${escapeHtml(l)}</p>`).join('')}</div>`;
680
752
  }
681
753
 
682
- // Danh sách múi giờ phổ biến (IANA) cho dropdown pill. Gọn đủ dùng cho khu vực chính.
754
+ // Danh sách múi giờ phổ biến (IANA) cho dropdown pill. Gọn - đủ dùng cho khu vực chính.
683
755
  const TZ_OPTIONS = [
684
756
  ['Asia/Ho_Chi_Minh', 'Việt Nam (UTC+7)'],
685
757
  ['Asia/Bangkok', 'Bangkok (UTC+7)'],
@@ -1041,7 +1113,7 @@ function botListPanel(bots) {
1041
1113
  function statusBadge(v) { return `<span class="runtime-badge ${v === 'online' ? 'ok' : v === 'unknown' ? 'warn' : 'bad'}">${v || 'offline'}</span>`; }
1042
1114
  function credentialField({ id, label, value = '', editable = false, placeholder = '' }) {
1043
1115
  const empty = !String(value || '').trim();
1044
- return `<label class="cred-field ${empty ? 'is-empty' : ''}"><span>${label}</span><div class="cred-input-wrap"><input id="${id}" name="${id}" type="password" ${editable ? '' : 'readonly'} autocomplete="off" value="${escapeHtml(value)}" placeholder="${escapeHtml(placeholder)}"/><button class="secondary cred-toggle" type="button" data-toggle-secret="${id}">${t('Hiện','Show')}</button></div>${empty && editable ? `<small>${t('Đang trống nhập key rồi lưu.', 'Empty enter key then save.')}</small>` : ''}</label>`;
1116
+ return `<label class="cred-field ${empty ? 'is-empty' : ''}"><span>${label}</span><div class="cred-input-wrap"><input id="${id}" name="${id}" type="password" ${editable ? '' : 'readonly'} autocomplete="off" value="${escapeHtml(value)}" placeholder="${escapeHtml(placeholder)}"/><button class="secondary cred-toggle" type="button" data-toggle-secret="${id}">${t('Hiện','Show')}</button></div>${empty && editable ? `<small>${t('Đang trống - nhập key rồi lưu.', 'Empty - enter key then save.')}</small>` : ''}</label>`;
1045
1117
  }
1046
1118
  function botStatusPanel(s) {
1047
1119
  const c = s.credentials || {};
@@ -1076,7 +1148,7 @@ function joinDisplayPath(root = '', child = '') {
1076
1148
  function projectPathLine(bot = currentBot(), fileName = '') {
1077
1149
  const s = state.install || {};
1078
1150
  // Legacy configs stored the container-absolute workspace; show it relative to the project root
1079
- // (new configs are already relative see bot-config-gen).
1151
+ // (new configs are already relative - see bot-config-gen).
1080
1152
  const workspace = String(bot?.workspace || '').replace(/^\/home\/node\/project\/?/, '');
1081
1153
  const relFile = fileName ? String(fileName).replace(/^\.openclaw[\\/][^\\/]+[\\/]?/, '') : '';
1082
1154
  const full = workspace ? joinDisplayPath(s.projectDir || '', relFile ? `${workspace}/${relFile}` : workspace) : (s.projectDir || '-');
@@ -1128,16 +1200,16 @@ function filesView() { return `<div class="files">${state.files.map(f=>`<article
1128
1200
  function botSkillsPanel() {
1129
1201
  const flags = state.featureFlags || {};
1130
1202
  const skills = [
1131
- { id: 'cron', title: 'Cron', desc: 'Native scheduler (SQLite) cron guide in TOOLS.md' },
1203
+ { id: 'cron', title: 'Cron', desc: 'Native scheduler (SQLite) - cron guide in TOOLS.md' },
1132
1204
  { id: 'image-gen', title: 'Tạo ảnh Infographic', desc: 'Tạo ảnh infographic, poster tự động qua 9Router' },
1133
1205
  { id: 'web-search', title: 'Web Search', desc: 'Tìm kiếm web thời gian thực (DuckDuckGo)' },
1134
1206
  ];
1135
1207
  const plugins = [
1136
- { id: 'zalo-connect', title: 'OpenClaw Zalo Connect', desc: t('Kênh Zalo cá nhân (zca-js) BẮT BUỘC cho bot Zalo. Tự cài khi tạo bot Zalo đầu tiên; bấm Cập nhật để lên bản mới nhất.', 'Personal Zalo channel (zca-js) REQUIRED for Zalo bots. Auto-installed with your first Zalo bot; click Update for the latest version.'), channels: ['zalo-personal'] },
1137
- { id: 'learning-memory', title: 'Siêu Trí Nhớ Dài Hạn (learning-memory)', desc: t('Bộ nhớ always-on: nạp MEMORY.md + USER.md đã chắt lọc vào MỌI lượt kể cả trong nhóm nên bot không quên ngữ cảnh/quy tắc. Tự cài khi tạo bot.', 'Always-on memory: injects a curated MEMORY.md + USER.md into every turn including group chats so the bot stops forgetting context and rules. Auto-installed on bot creation.') },
1208
+ { id: 'zalo-connect', title: 'OpenClaw Zalo Connect', desc: t('Kênh Zalo cá nhân (zca-js) - BẮT BUỘC cho bot Zalo. Tự cài khi tạo bot Zalo đầu tiên; bấm Cập nhật để lên bản mới nhất.', 'Personal Zalo channel (zca-js) - REQUIRED for Zalo bots. Auto-installed with your first Zalo bot; click Update for the latest version.'), channels: ['zalo-personal'] },
1209
+ { id: 'learning-memory', title: 'Siêu Trí Nhớ Dài Hạn (learning-memory)', desc: t('Bộ nhớ always-on: nạp MEMORY.md + USER.md đã chắt lọc vào MỌI lượt - kể cả trong nhóm - nên bot không quên ngữ cảnh/quy tắc. Tự cài khi tạo bot.', 'Always-on memory: injects a curated MEMORY.md + USER.md into every turn - including group chats - so the bot stops forgetting context and rules. Auto-installed on bot creation.') },
1138
1210
  { id: 'openclaw-browser-automation', title: 'openclaw-browser-automation', desc: 'Smart Search + Browser (headless & Chrome thật)' },
1139
1211
  { id: 'openclaw-zalo-mod', title: 'openclaw-zalo-mod', desc: 'Zalo group helpers', channels: ['zalo-personal'], openWebPort: 18790, openWebPath: '/dashboard' },
1140
- { id: 'openclaw-fb-messenger', title: 'openclaw-fb-messenger', desc: t('Kênh Facebook Messenger webhook + Graph API (bắt buộc cho bot Messenger)', 'Facebook Messenger channel webhook + Graph API (required for Messenger bots)'), channels: ['fb-messenger'] },
1212
+ { id: 'openclaw-fb-messenger', title: 'openclaw-fb-messenger', desc: t('Kênh Facebook Messenger - webhook + Graph API (bắt buộc cho bot Messenger)', 'Facebook Messenger channel - webhook + Graph API (required for Messenger bots)'), channels: ['fb-messenger'] },
1141
1213
  { id: 'openclaw-facebook-crawler', title: 'openclaw-facebook-crawler', desc: 'Facebook crawler automation', channels: ['fb-messenger'] },
1142
1214
  { id: 'openclaw-n8n-facebook-poster', title: 'openclaw-n8n-facebook-poster', desc: 'Facebook post automation (n8n)', channels: ['fb-messenger'] },
1143
1215
  ];
@@ -1165,10 +1237,10 @@ function botSkillsPanel() {
1165
1237
  let toggleHtml = '';
1166
1238
  const secs = [];
1167
1239
  if (isInstalled) {
1168
- 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>`;
1240
+ 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>`;
1169
1241
  if (item.openWebPort) {
1170
1242
  // zalo-mod's dashboard is always gateway port + 1 (docker 18790, native 18890, or whatever
1171
- // the SELECTED project's gateway runs on) never the hardcoded default. Derive it from the
1243
+ // the SELECTED project's gateway runs on) - never the hardcoded default. Derive it from the
1172
1244
  // active project's gatewayUrl so every project opens its own dashboard.
1173
1245
  let gwPort = 0;
1174
1246
  try { gwPort = parseInt(new URL(state.install?.gatewayUrl).port, 10) || 0; } catch {}
@@ -1249,7 +1321,11 @@ function wireTab() {
1249
1321
  document.querySelectorAll('[data-confirm-action]').forEach(el => el.onclick = () => withButtonLoading(el, async () => {
1250
1322
  const action = el.dataset.confirmAction;
1251
1323
  const m = state.confirmModal;
1252
- if (action === 'cancel' || !m) { state.confirmModal = null; render(); return; }
1324
+ if (action === 'cancel' || !m) {
1325
+ // Declining can be meaningful - "ask me later" has to remember that it was asked.
1326
+ if (m && typeof m.onCancel === 'function') { await m.onCancel(); return; }
1327
+ state.confirmModal = null; render(); return;
1328
+ }
1253
1329
  if (action === 'ok' && typeof m.onConfirm === 'function') await m.onConfirm();
1254
1330
  }));
1255
1331
  // Screen-recording / accessibility grants: only the OS can give these, so the button opens the
@@ -1258,7 +1334,7 @@ function wireTab() {
1258
1334
  const kind = btn.dataset.hostPerm;
1259
1335
  try {
1260
1336
  const r = await api('/api/host/permissions', { method: 'POST', body: { kind, projectDir: activeProjectDir() } });
1261
- 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');
1337
+ 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');
1262
1338
  const granted = r.screen && r.screen.supported ? r.screen.granted : null;
1263
1339
  showToast(t('Đã mở cài đặt quyền','Opened settings'),
1264
1340
  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.')
@@ -1311,7 +1387,7 @@ function wireTab() {
1311
1387
  state.install = { ...(state.install || {}), ...result };
1312
1388
  autoSwitchBotChannel();
1313
1389
  render();
1314
- // Background: full refresh (versions, files, flags) UI already shows the new project.
1390
+ // Background: full refresh (versions, files, flags) - UI already shows the new project.
1315
1391
  await loadSystem(true);
1316
1392
  await loadStatus(true);
1317
1393
  autoSwitchBotChannel();
@@ -1396,9 +1472,9 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1396
1472
  }));
1397
1473
  document.querySelectorAll('[data-bot-add-mount]').forEach(btn => btn.onclick = () => openPathModal({
1398
1474
  title: t('Cấp quyền ổ đĩa/thư mục','Grant disk/folder access'),
1399
- message: t('Nhập đường dẫn host (vd /Users/ban/Documents hoặc D:/data). Mount cho MỌI bot trong project rồi tự recreate container để áp dụng (mất ~1020s).','Enter a host path (e.g. /Users/you/Documents or D:/data). Mounts for ALL bots in this project, then auto-recreates the container to apply (~1020s).'),
1475
+ message: t('Nhập đường dẫn host (vd /Users/ban/Documents hoặc D:/data). Mount cho MỌI bot trong project rồi tự recreate container để áp dụng (mất ~10-20s).','Enter a host path (e.g. /Users/you/Documents or D:/data). Mounts for ALL bots in this project, then auto-recreates the container to apply (~10-20s).'),
1400
1476
  placeholder: '/Users/ban/Documents',
1401
- field2: { label: t('Tên mount (tùy chọn) sẽ thành /mnt/<tên>. Bỏ trống = tự lấy theo tên thư mục.','Mount name (optional) becomes /mnt/<name>. Empty = derived from folder name.'), placeholder: 'baocao' },
1477
+ field2: { label: t('Tên mount (tùy chọn) - sẽ thành /mnt/<tên>. Bỏ trống = tự lấy theo tên thư mục.','Mount name (optional) - becomes /mnt/<name>. Empty = derived from folder name.'), placeholder: 'baocao' },
1402
1478
  onConfirm: async (val, name) => {
1403
1479
  const hostPath = String(val || '').trim();
1404
1480
  if (!hostPath) return;
@@ -1619,7 +1695,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1619
1695
  $('#bot-create')?.addEventListener('submit', async (ev) => {
1620
1696
  ev.preventDefault();
1621
1697
  // Facebook Messenger needs the fb-messenger plugin (public on ClawHub). We don't block
1622
- // creation the heads-up modal on channel-select tells the user to install it from the
1698
+ // creation - the heads-up modal on channel-select tells the user to install it from the
1623
1699
  // bot's Plugins section afterwards (installable via the openclaw-fb-messenger card).
1624
1700
  const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
1625
1701
  if (submitBtn?.classList.contains('is-loading')) return;
@@ -1630,7 +1706,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1630
1706
  body.channel = state.botChannel || 'telegram';
1631
1707
  body.userTimezone = state.tz;
1632
1708
  // Only the CREATE flow starts a Zalo QR login (the server kicks it off and sets
1633
- // loginStarted). Editing a bot (rename, persona...) must never pop the QR modal
1709
+ // loginStarted). Editing a bot (rename, persona...) must never pop the QR modal -
1634
1710
  // the session is already saved and the PUT endpoint starts no login.
1635
1711
  if (body.channel === 'zalo-personal' && !state.botEditId) {
1636
1712
  state.botModalOpen = false;
@@ -1644,7 +1720,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1644
1720
  const url = isEdit ? `/api/bot/${encodeURIComponent(state.botEditId)}` : '/api/bot/create';
1645
1721
  const method = isEdit ? 'PUT' : 'POST';
1646
1722
  const result = await api(url, { method, body });
1647
- state.botMessage = `✅ ${isEdit ? t('Đã cập nhật','Updated') : t('Đã tạo','Created')} ${result.agentId}${result.warning ? ' ' + result.warning : ''}`;
1723
+ state.botMessage = `✅ ${isEdit ? t('Đã cập nhật','Updated') : t('Đã tạo','Created')} ${result.agentId}${result.warning ? ' - ' + result.warning : ''}`;
1648
1724
  showToast(isEdit ? t('Đã cập nhật', 'Updated') : t('Đã tạo bot', 'Bot created'), `${isEdit ? t('Đã cập nhật','Updated') : t('Đã tạo','Created')} bot ${result.agentId} thành công!`, 'success');
1649
1725
  state.botChannel = body.channel;
1650
1726
  state.botEditId = '';
@@ -1689,6 +1765,9 @@ async function loadStatus(silent=false){
1689
1765
  state.install = install;
1690
1766
  if (!state.selectedProjectDir && state.install?.projectDir) state.selectedProjectDir = state.install.projectDir;
1691
1767
  if (!silent) render();
1768
+ // Status is the first place we can actually see which runtime this project uses, so this is
1769
+ // where the Docker project gets told there is a better place to be.
1770
+ maybeOfferNativeMigration();
1692
1771
  }
1693
1772
  function autoSwitchBotChannel() {
1694
1773
  const bots = state.install?.bots || [];
@@ -1771,7 +1850,7 @@ function updateZaloHealthDom() {
1771
1850
  });
1772
1851
  }
1773
1852
  // Live status: poll the health endpoint while Zalo bots are on screen (the server caches
1774
- // the ~3s CLI probe with a short TTL, so this stays cheap) and patch the badges in place
1853
+ // the ~3s CLI probe with a short TTL, so this stays cheap) and patch the badges in place -
1775
1854
  // no more pressing "Làm mới" to see a bot drop or come back.
1776
1855
  const ZALO_HEALTH_POLL_MS = 10000;
1777
1856
  let zaloHealthPollBusy = false;
@@ -1811,7 +1811,7 @@ html[data-theme="light"] .bottom .nav.active {
1811
1811
  text-align: right !important;
1812
1812
  }
1813
1813
 
1814
- /* Compact per-account Zalo health shared versions stay in the right status column. */
1814
+ /* Compact per-account Zalo health - shared versions stay in the right status column. */
1815
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) */
@@ -1901,7 +1901,7 @@ html[data-theme="light"] .bot-create-card:hover {
1901
1901
  @media(max-width:760px){
1902
1902
  .bot-modal-backdrop{padding:12px 12px 80px!important}
1903
1903
  .bot-modal{max-height:calc(100dvh - 96px)!important}
1904
- /* 2-up channel picker on phones less scrolling than a single column */
1904
+ /* 2-up channel picker on phones - less scrolling than a single column */
1905
1905
  .bot-modal .bot-channel-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}
1906
1906
  }
1907
1907
 
@@ -1944,14 +1944,14 @@ main{transition:margin .2s}
1944
1944
  Mobile UX refinements (user feedback)
1945
1945
  ════════════════════════════════════════════════════════════════════════════ */
1946
1946
 
1947
- /* [Hero] Dashboard already shows its title inside the hero card drop the duplicate
1947
+ /* [Hero] Dashboard already shows its title inside the hero card - drop the duplicate
1948
1948
  page-title header so it doesn't repeat "Dashboard vận hành" twice. */
1949
1949
  body[data-active-tab="dashboard"] .top{display:none}
1950
1950
 
1951
1951
  /* [Modals] While any modal is open, hide the fixed bottom nav so it can't cover the modal's
1952
1952
  lower content (and footer buttons) on mobile. The backdrop owns the screen at that point. */
1953
1953
  body:has(.modal-backdrop) .bottom{display:none!important}
1954
- /* Bot / Cài đặt hero buttons sit side-by-side keep their labels on one line. */
1954
+ /* Bot / Cài đặt hero buttons sit side-by-side - keep their labels on one line. */
1955
1955
  .dash-actions .icon-btn2{white-space:nowrap}
1956
1956
 
1957
1957
  /* [Confirm modal] Optional body block for step-by-step guides (e.g. Chrome-debug on a VPS):
@@ -1994,7 +1994,7 @@ body:has(.modal-backdrop) .bottom{display:none!important}
1994
1994
  /* [Connect project] The action buttons were absolutely positioned and overlapped the
1995
1995
  path text. Put the card into normal flow with full-width buttons below the content.
1996
1996
  The card used overflow:hidden + a fixed min-height and the list capped at 260px with an
1997
- inner scroll all of which clipped the now-in-flow buttons. Let everything grow. */
1997
+ inner scroll - all of which clipped the now-in-flow buttons. Let everything grow. */
1998
1998
  .detected-projects{max-height:none!important;overflow:visible!important}
1999
1999
  .detected-project{padding:12px!important;min-height:0!important;height:auto!important;overflow:visible!important}
2000
2000
  .detected-project__actions{position:static!important;transform:none!important;width:100%;margin-top:4px}
@@ -2031,7 +2031,7 @@ body:has(.modal-backdrop) .bottom{display:none!important}
2031
2031
 
2032
2032
  @media(max-width:520px){
2033
2033
  /* [Setup OS/mode] Shrink the logo cards: keep them 2-up (compact, less scrolling) with
2034
- much smaller icons just large enough to tap comfortably. */
2034
+ much smaller icons - just large enough to tap comfortably. */
2035
2035
  .os-grid,.mode-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}
2036
2036
  .logo-card{min-height:0!important;gap:8px!important;padding:12px 8px!important;border-radius:14px!important}
2037
2037
  .logo-card .choice-card__icon{width:40px!important;height:40px!important;border-radius:11px!important}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.17.1",
3
+ "version": "5.17.2",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {