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/README.md +62 -62
- package/README.vi.md +57 -57
- package/dist/cli.js +4 -4
- package/dist/server/local-server.js +249 -210
- package/dist/setup/data/channels.js +7 -7
- package/dist/setup/data/header.js +1 -1
- package/dist/setup/data/index.js +1 -1
- package/dist/setup/data/plugins.js +3 -3
- package/dist/setup/data/providers.js +7 -7
- package/dist/setup/shared/bot-config-gen.js +34 -34
- package/dist/setup/shared/common-gen.js +6 -6
- package/dist/setup/shared/docker-gen.js +23 -23
- package/dist/setup/shared/host-ui-ps1.js +1 -1
- package/dist/setup/shared/install-gen.js +9 -9
- package/dist/setup/shared/windows-launcher-gen.js +22 -8
- package/dist/setup/shared/workspace-gen.js +144 -144
- package/dist/web/app.js +112 -33
- package/dist/web/styles.css +6 -6
- package/package.json +1 -1
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
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
649
|
-
bodyHtml: `<p class="guide-step">1. ${t('Mở Chrome debug trên máy bạn
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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) {
|
|
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 đó
|
|
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)
|
|
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 ~10
|
|
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)
|
|
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
|
|
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 ? '
|
|
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;
|
package/dist/web/styles.css
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|