create-openclaw-bot 5.17.0 → 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 +63 -63
- package/README.vi.md +58 -58
- package/dist/cli.js +4 -4
- package/dist/server/local-server.js +702 -1107
- 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 +51 -11
- package/dist/setup/shared/workspace-gen.js +144 -144
- package/dist/web/app.js +152 -72
- 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,50 +321,128 @@ 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
|
-
const
|
|
328
|
-
const
|
|
329
|
-
const cli = (r.commands || []).includes('codex');
|
|
327
|
+
const cu = r.computerUse || {};
|
|
328
|
+
const isMac = /mac/i.test(state.system?.os || state.os || '');
|
|
330
329
|
const line = (icon, text) => `<li><span aria-hidden="true">${icon}</span><div>${text}</div></li>`;
|
|
330
|
+
|
|
331
|
+
// OpenClaw's own `computer` tool is the real thing here: screenshot, move, click, type, drag.
|
|
332
|
+
// Codex is only a fallback for machines that have the ChatGPT app, so it must not lead - the
|
|
333
|
+
// old copy said "mouse/keyboard control is macOS-only", which stopped being true once the
|
|
334
|
+
// cua-computer plugin shipped for Windows.
|
|
331
335
|
const statusItems = [
|
|
332
|
-
|
|
333
|
-
? line('✅', t('Bot
|
|
334
|
-
: line('⚠️', t(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
? 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>.'))
|
|
340
|
-
: 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.')),
|
|
341
|
-
(r.granted && r.granted.length)
|
|
342
|
-
? line('✅', t(`Đã cấp quyền chạy: ${escapeHtml(r.granted.join(', '))}.`, `Granted: ${escapeHtml(r.granted.join(', '))}.`))
|
|
336
|
+
cu.ok && cu.enabled
|
|
337
|
+
? line('✅', t('Bot <b>nhìn và điều khiển được máy này</b>: chụp màn hình, rê chuột, bấm chuột, gõ phím, kéo thả.', 'Your bot <b>can see and drive this machine</b>: screenshot, move, click, type, drag.'))
|
|
338
|
+
: line('⚠️', t(`Chưa bật được điều khiển máy: ${escapeHtml(cu.error || 'không rõ lý do')}`, `Could not enable computer control: ${escapeHtml(cu.error || 'unknown reason')}`)),
|
|
339
|
+
// No app list any more: the bot opens things the way a person does - Start menu, type the
|
|
340
|
+
// name, Enter - so it is not limited to whatever we managed to enumerate.
|
|
341
|
+
cu.ok && cu.enabled
|
|
342
|
+
? line('✅', t('Mở được <b>mọi ứng dụng đang cài</b> trên máy, kể cả app mới cài hôm qua.', 'Can open <b>any installed app</b>, including one installed yesterday.'))
|
|
343
343
|
: '',
|
|
344
344
|
].filter(Boolean).join('');
|
|
345
|
+
|
|
346
|
+
// Windows needs no privacy prompts; macOS does, and only from System Settings.
|
|
347
|
+
const permBlock = isMac ? `
|
|
348
|
+
<h4>${t('Cấp quyền màn hình cho máy','Grant the OS screen permissions')}</h4>
|
|
349
|
+
<p>${t('macOS chỉ cấp <b>Screen Recording</b> và <b>Accessibility</b> từ System Settings. Bấm nút dưới rồi bật cho <code>node</code>.','macOS only grants <b>Screen Recording</b> and <b>Accessibility</b> from System Settings. Click below, then tick <code>node</code>.')}</p>
|
|
350
|
+
<p class="cu-perm-row">
|
|
351
|
+
<button class="secondary cu-btn" type="button" data-host-perm="screen">${t('Chụp/quay màn hình','Screen recording')}</button>
|
|
352
|
+
<button class="secondary cu-btn" type="button" data-host-perm="accessibility">${t('Accessibility (chuột/bàn phím)','Accessibility (mouse/keys)')}</button>
|
|
353
|
+
</p>` : '';
|
|
354
|
+
|
|
345
355
|
state.confirmModal = {
|
|
346
356
|
icon: '🖥️',
|
|
347
357
|
eyebrow: t('Điều khiển máy','PC control'),
|
|
348
|
-
title: t('
|
|
358
|
+
title: cu.ok && cu.enabled ? t('Đã bật điều khiển máy','Computer control is on') : t('Bật chưa xong','Not fully enabled'),
|
|
349
359
|
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.'),
|
|
350
360
|
bodyHtml: `
|
|
351
361
|
<ul class="cu-status">${statusItems}</ul>
|
|
352
|
-
|
|
353
|
-
<p>${t('
|
|
354
|
-
<p class="cu-
|
|
355
|
-
|
|
356
|
-
<button class="secondary cu-btn" type="button" data-host-perm="accessibility">${t('Accessibility (chuột/bàn phím)','Accessibility (mouse/keys)')}</button>
|
|
357
|
-
</p>
|
|
358
|
-
<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>
|
|
359
|
-
<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>
|
|
362
|
+
${permBlock}
|
|
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
|
+
<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('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>
|
|
360
366
|
`,
|
|
361
367
|
okText: t('Đã hiểu','Got it'),
|
|
362
368
|
okDanger: false,
|
|
363
369
|
hideCancel: true,
|
|
364
370
|
onConfirm: () => { state.confirmModal = null; render(); },
|
|
365
371
|
};
|
|
372
|
+
}
|
|
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
|
+
};
|
|
366
443
|
render();
|
|
367
444
|
}
|
|
445
|
+
|
|
368
446
|
async function pickFolderPathShared() {
|
|
369
447
|
try {
|
|
370
448
|
const picked = await api('/api/project/pick-folder', { method: 'POST', body: {} });
|
|
@@ -398,7 +476,7 @@ function isVersionNewer(a, b) {
|
|
|
398
476
|
}
|
|
399
477
|
|
|
400
478
|
// Build a URL on the SAME host the dashboard is currently viewed from, so "Open" works
|
|
401
|
-
// 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
|
|
402
480
|
// points at the viewer's own localhost when browsing from another machine. Port comes from
|
|
403
481
|
// the backend-provided URL (which carries the right port) or the fallback.
|
|
404
482
|
function sameHostUrl(rawUrl, fallbackPort) {
|
|
@@ -409,7 +487,7 @@ function sameHostUrl(rawUrl, fallbackPort) {
|
|
|
409
487
|
return `${proto}//${host}:${port}`;
|
|
410
488
|
}
|
|
411
489
|
|
|
412
|
-
// "Open from another machine" helper
|
|
490
|
+
// "Open from another machine" helper - builds a ready SSH-tunnel command (auto-filled
|
|
413
491
|
// with the server's public IP + the dashboard/gateway/9router/zalo-mod ports) so any user
|
|
414
492
|
// on a VPS can reach the web UIs from their own computer without knowing how to set it up.
|
|
415
493
|
function remoteAccessPanel(s = {}) {
|
|
@@ -418,7 +496,7 @@ function remoteAccessPanel(s = {}) {
|
|
|
418
496
|
const host = r.host || '<your-server-ip>';
|
|
419
497
|
const user = r.user || 'root';
|
|
420
498
|
const portOf = (raw, def) => { try { return new URL(raw).port || def; } catch { return def; } };
|
|
421
|
-
// 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
|
|
422
500
|
// "Open" button uses. Hardcoding it meant any project whose gateway is not on 18789 got a tunnel
|
|
423
501
|
// command missing the dashboard port, and the dashboard then simply failed to load with no clue why.
|
|
424
502
|
const gwPort = Number(portOf(s.gatewayUrl, 18789));
|
|
@@ -426,7 +504,7 @@ function remoteAccessPanel(s = {}) {
|
|
|
426
504
|
const cmd = `ssh ${ports.map((p) => `-L ${p}:127.0.0.1:${p}`).join(' ')} ${user}@${host}`;
|
|
427
505
|
return `<details class="card" style="margin-top:12px;" ${r.headless ? 'open' : ''}>
|
|
428
506
|
<summary style="cursor:pointer; font-weight:600;">🌐 ${t('Mở từ máy khác (VPS/server)', 'Open from another machine (VPS/server)')}</summary>
|
|
429
|
-
<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>
|
|
430
508
|
<div style="display:flex; gap:8px;">
|
|
431
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;" />
|
|
432
510
|
<button class="secondary icon-btn2" type="button" data-copy="${escapeHtml(cmd)}">${actionIcon('link')}<span>${t('Copy', 'Copy')}</span></button>
|
|
@@ -438,7 +516,7 @@ function remoteAccessPanel(s = {}) {
|
|
|
438
516
|
function topbarActionsHtml() {
|
|
439
517
|
const setupVer = state.system?.versions?.setup;
|
|
440
518
|
const latestSetupVer = state.system?.versions?.latestSetup;
|
|
441
|
-
// 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 -
|
|
442
520
|
// a local/dev build can be ahead of what's published).
|
|
443
521
|
const hasNewVersion = !!(setupVer && latestSetupVer && isVersionNewer(latestSetupVer, setupVer));
|
|
444
522
|
return `
|
|
@@ -460,7 +538,7 @@ function topbarActionsHtml() {
|
|
|
460
538
|
<button class="seg__btn ${state.lang==='en'?'is-active':''}" data-pref="lang" data-value="en">EN</button>
|
|
461
539
|
</div>
|
|
462
540
|
${hasNewVersion ? `
|
|
463
|
-
<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;">
|
|
464
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>
|
|
465
543
|
<span>${t('Cập nhật', 'Update')} ${latestSetupVer}</span>
|
|
466
544
|
</button>
|
|
@@ -634,13 +712,13 @@ function wireSkillsHandlers(scope = document) {
|
|
|
634
712
|
try {
|
|
635
713
|
const r = await api('/api/browser/start-chrome', { method: 'POST', body: {} });
|
|
636
714
|
if (r.headless) {
|
|
637
|
-
// VPS has no display
|
|
715
|
+
// VPS has no display - guide the user to run Chrome on THEIR machine + a reverse tunnel.
|
|
638
716
|
state.confirmModal = {
|
|
639
717
|
icon: '🌐',
|
|
640
718
|
eyebrow: t('Chrome debug', 'Chrome debug'),
|
|
641
719
|
title: t('Dùng Chrome trên máy của bạn', 'Use Chrome on your computer'),
|
|
642
|
-
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
|
|
643
|
-
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>`,
|
|
644
722
|
okText: t('Đã hiểu', 'Got it'),
|
|
645
723
|
okDanger: false,
|
|
646
724
|
hideCancel: true,
|
|
@@ -648,10 +726,10 @@ function wireSkillsHandlers(scope = document) {
|
|
|
648
726
|
};
|
|
649
727
|
render();
|
|
650
728
|
} else {
|
|
651
|
-
// 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 -
|
|
652
730
|
// say so, because the old flow required closing every Chrome window first.
|
|
653
731
|
showToast(t('Đã mở Chrome cho bot', 'Chrome opened for the bot'),
|
|
654
|
-
`${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');
|
|
655
733
|
}
|
|
656
734
|
} catch (err) {
|
|
657
735
|
showToast(t('Thất bại', 'Failed'), err.message, 'error');
|
|
@@ -673,7 +751,7 @@ function content() {
|
|
|
673
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>`;
|
|
674
752
|
}
|
|
675
753
|
|
|
676
|
-
// 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.
|
|
677
755
|
const TZ_OPTIONS = [
|
|
678
756
|
['Asia/Ho_Chi_Minh', 'Việt Nam (UTC+7)'],
|
|
679
757
|
['Asia/Bangkok', 'Bangkok (UTC+7)'],
|
|
@@ -1035,7 +1113,7 @@ function botListPanel(bots) {
|
|
|
1035
1113
|
function statusBadge(v) { return `<span class="runtime-badge ${v === 'online' ? 'ok' : v === 'unknown' ? 'warn' : 'bad'}">${v || 'offline'}</span>`; }
|
|
1036
1114
|
function credentialField({ id, label, value = '', editable = false, placeholder = '' }) {
|
|
1037
1115
|
const empty = !String(value || '').trim();
|
|
1038
|
-
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>`;
|
|
1039
1117
|
}
|
|
1040
1118
|
function botStatusPanel(s) {
|
|
1041
1119
|
const c = s.credentials || {};
|
|
@@ -1070,7 +1148,7 @@ function joinDisplayPath(root = '', child = '') {
|
|
|
1070
1148
|
function projectPathLine(bot = currentBot(), fileName = '') {
|
|
1071
1149
|
const s = state.install || {};
|
|
1072
1150
|
// Legacy configs stored the container-absolute workspace; show it relative to the project root
|
|
1073
|
-
// (new configs are already relative
|
|
1151
|
+
// (new configs are already relative - see bot-config-gen).
|
|
1074
1152
|
const workspace = String(bot?.workspace || '').replace(/^\/home\/node\/project\/?/, '');
|
|
1075
1153
|
const relFile = fileName ? String(fileName).replace(/^\.openclaw[\\/][^\\/]+[\\/]?/, '') : '';
|
|
1076
1154
|
const full = workspace ? joinDisplayPath(s.projectDir || '', relFile ? `${workspace}/${relFile}` : workspace) : (s.projectDir || '-');
|
|
@@ -1122,16 +1200,16 @@ function filesView() { return `<div class="files">${state.files.map(f=>`<article
|
|
|
1122
1200
|
function botSkillsPanel() {
|
|
1123
1201
|
const flags = state.featureFlags || {};
|
|
1124
1202
|
const skills = [
|
|
1125
|
-
{ id: 'cron', title: 'Cron', desc: 'Native scheduler (SQLite)
|
|
1203
|
+
{ id: 'cron', title: 'Cron', desc: 'Native scheduler (SQLite) - cron guide in TOOLS.md' },
|
|
1126
1204
|
{ id: 'image-gen', title: 'Tạo ảnh Infographic', desc: 'Tạo ảnh infographic, poster tự động qua 9Router' },
|
|
1127
1205
|
{ id: 'web-search', title: 'Web Search', desc: 'Tìm kiếm web thời gian thực (DuckDuckGo)' },
|
|
1128
1206
|
];
|
|
1129
1207
|
const plugins = [
|
|
1130
|
-
{ id: 'zalo-connect', title: 'OpenClaw Zalo Connect', desc: t('Kênh Zalo cá nhân (zca-js)
|
|
1131
|
-
{ 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.') },
|
|
1132
1210
|
{ id: 'openclaw-browser-automation', title: 'openclaw-browser-automation', desc: 'Smart Search + Browser (headless & Chrome thật)' },
|
|
1133
1211
|
{ id: 'openclaw-zalo-mod', title: 'openclaw-zalo-mod', desc: 'Zalo group helpers', channels: ['zalo-personal'], openWebPort: 18790, openWebPath: '/dashboard' },
|
|
1134
|
-
{ 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'] },
|
|
1135
1213
|
{ id: 'openclaw-facebook-crawler', title: 'openclaw-facebook-crawler', desc: 'Facebook crawler automation', channels: ['fb-messenger'] },
|
|
1136
1214
|
{ id: 'openclaw-n8n-facebook-poster', title: 'openclaw-n8n-facebook-poster', desc: 'Facebook post automation (n8n)', channels: ['fb-messenger'] },
|
|
1137
1215
|
];
|
|
@@ -1159,10 +1237,10 @@ function botSkillsPanel() {
|
|
|
1159
1237
|
let toggleHtml = '';
|
|
1160
1238
|
const secs = [];
|
|
1161
1239
|
if (isInstalled) {
|
|
1162
|
-
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>`;
|
|
1163
1241
|
if (item.openWebPort) {
|
|
1164
1242
|
// zalo-mod's dashboard is always gateway port + 1 (docker 18790, native 18890, or whatever
|
|
1165
|
-
// the SELECTED project's gateway runs on)
|
|
1243
|
+
// the SELECTED project's gateway runs on) - never the hardcoded default. Derive it from the
|
|
1166
1244
|
// active project's gatewayUrl so every project opens its own dashboard.
|
|
1167
1245
|
let gwPort = 0;
|
|
1168
1246
|
try { gwPort = parseInt(new URL(state.install?.gatewayUrl).port, 10) || 0; } catch {}
|
|
@@ -1243,7 +1321,11 @@ function wireTab() {
|
|
|
1243
1321
|
document.querySelectorAll('[data-confirm-action]').forEach(el => el.onclick = () => withButtonLoading(el, async () => {
|
|
1244
1322
|
const action = el.dataset.confirmAction;
|
|
1245
1323
|
const m = state.confirmModal;
|
|
1246
|
-
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
|
+
}
|
|
1247
1329
|
if (action === 'ok' && typeof m.onConfirm === 'function') await m.onConfirm();
|
|
1248
1330
|
}));
|
|
1249
1331
|
// Screen-recording / accessibility grants: only the OS can give these, so the button opens the
|
|
@@ -1252,12 +1334,12 @@ function wireTab() {
|
|
|
1252
1334
|
const kind = btn.dataset.hostPerm;
|
|
1253
1335
|
try {
|
|
1254
1336
|
const r = await api('/api/host/permissions', { method: 'POST', body: { kind, projectDir: activeProjectDir() } });
|
|
1255
|
-
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');
|
|
1256
1338
|
const granted = r.screen && r.screen.supported ? r.screen.granted : null;
|
|
1257
1339
|
showToast(t('Đã mở cài đặt quyền','Opened settings'),
|
|
1258
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.')
|
|
1259
|
-
: granted === false ? t('Chưa có quyền
|
|
1260
|
-
: t('Bật quyền cho "node"
|
|
1341
|
+
: granted === false ? t('Chưa có quyền. Bật cho "node" trong danh sách vừa mở, rồi khởi động lại bot.','Not granted yet. Tick "node" in the list that just opened, then restart the bot.')
|
|
1342
|
+
: t('Bật quyền cho "node" trong danh sách vừa mở.','Tick "node" in the list that just opened.'),
|
|
1261
1343
|
granted === false ? 'error' : 'success', 8000);
|
|
1262
1344
|
} catch (err) { showToast(t('Thất bại','Failed'), err.message, 'error'); }
|
|
1263
1345
|
}));
|
|
@@ -1305,7 +1387,7 @@ function wireTab() {
|
|
|
1305
1387
|
state.install = { ...(state.install || {}), ...result };
|
|
1306
1388
|
autoSwitchBotChannel();
|
|
1307
1389
|
render();
|
|
1308
|
-
// Background: full refresh (versions, files, flags)
|
|
1390
|
+
// Background: full refresh (versions, files, flags) - UI already shows the new project.
|
|
1309
1391
|
await loadSystem(true);
|
|
1310
1392
|
await loadStatus(true);
|
|
1311
1393
|
autoSwitchBotChannel();
|
|
@@ -1390,9 +1472,9 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1390
1472
|
}));
|
|
1391
1473
|
document.querySelectorAll('[data-bot-add-mount]').forEach(btn => btn.onclick = () => openPathModal({
|
|
1392
1474
|
title: t('Cấp quyền ổ đĩa/thư mục','Grant disk/folder access'),
|
|
1393
|
-
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).'),
|
|
1394
1476
|
placeholder: '/Users/ban/Documents',
|
|
1395
|
-
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' },
|
|
1396
1478
|
onConfirm: async (val, name) => {
|
|
1397
1479
|
const hostPath = String(val || '').trim();
|
|
1398
1480
|
if (!hostPath) return;
|
|
@@ -1410,23 +1492,18 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1410
1492
|
try { cur = await api('/api/host/control' + projectQuery({})); }
|
|
1411
1493
|
catch (err) { return showToast(t('Thất bại','Failed'), err.message, 'error'); }
|
|
1412
1494
|
const on = !!cur.enabled;
|
|
1413
|
-
|
|
1414
|
-
const grants = cur.grants || [];
|
|
1415
|
-
const chips = (items) => items.map((i) => `<code>${escapeHtml(i)}</code>`).join(' ');
|
|
1416
|
-
const scripts = grants.filter((g) => ['node', 'npx', 'codex'].includes(g));
|
|
1417
|
-
// One row per capability instead of a wall of prose — the operator is granting real access to
|
|
1495
|
+
// One row per capability instead of a wall of prose - the operator is granting real access to
|
|
1418
1496
|
// their machine and should be able to see, at a glance, exactly what each row means.
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
];
|
|
1497
|
+
// There is no app allow-list to show: the bot drives the screen, so it reaches anything the
|
|
1498
|
+
// person sitting at this machine could reach. Say that plainly rather than implying limits.
|
|
1499
|
+
const rows = cur.native ? [
|
|
1500
|
+
{ icon: '📸', title: t('Nhìn màn hình','See the screen'), desc: t('bot chụp được màn hình khi bạn nhờ','the bot can screenshot your screen when you ask') },
|
|
1501
|
+
{ icon: '🖱', title: t('Điều khiển chuột/bàn phím','Mouse & keyboard control'), desc: t('bot rê chuột, bấm, gõ phím và kéo thả trên máy này','the bot moves the mouse, clicks, types and drags on this machine') },
|
|
1502
|
+
{ icon: '🗂', title: t('Mở ứng dụng','Open apps'), desc: t('mọi app đang cài, vì bot thao tác như người ngồi trước máy','any installed app, because it operates the machine like a person would') },
|
|
1503
|
+
] : [];
|
|
1427
1504
|
const bodyHtml = on ? '' : `
|
|
1428
1505
|
<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>
|
|
1429
|
-
<p class="cu-note">${t('
|
|
1506
|
+
<p class="cu-note">${t('Chỉ dùng trên máy có màn hình (không áp dụng VPS headless).','Desktop only (not a headless VPS).')}</p>`;
|
|
1430
1507
|
state.confirmModal = {
|
|
1431
1508
|
icon: '🖥️',
|
|
1432
1509
|
eyebrow: t('Điều khiển máy','PC control'),
|
|
@@ -1439,7 +1516,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1439
1516
|
okDanger: on,
|
|
1440
1517
|
onConfirm: async () => {
|
|
1441
1518
|
state.confirmModal = null; render();
|
|
1442
|
-
if (!on) showToast(t('Đang bật','Enabling'), t('Đang cấp quyền
|
|
1519
|
+
if (!on) showToast(t('Đang bật','Enabling'), t('Đang cấp quyền và khởi động node điều khiển (có thể mất ~30s)…','Granting access and starting the control node (may take ~30s)…'), 'success');
|
|
1443
1520
|
try {
|
|
1444
1521
|
const r = await api('/api/host/control', { method: 'POST', body: { enabled: !on, projectDir: activeProjectDir() } });
|
|
1445
1522
|
if (!on && r.started && r.started.ok === false && r.started.reason) {
|
|
@@ -1618,7 +1695,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1618
1695
|
$('#bot-create')?.addEventListener('submit', async (ev) => {
|
|
1619
1696
|
ev.preventDefault();
|
|
1620
1697
|
// Facebook Messenger needs the fb-messenger plugin (public on ClawHub). We don't block
|
|
1621
|
-
// creation
|
|
1698
|
+
// creation - the heads-up modal on channel-select tells the user to install it from the
|
|
1622
1699
|
// bot's Plugins section afterwards (installable via the openclaw-fb-messenger card).
|
|
1623
1700
|
const submitBtn = ev.currentTarget.querySelector('button[type="submit"]');
|
|
1624
1701
|
if (submitBtn?.classList.contains('is-loading')) return;
|
|
@@ -1629,7 +1706,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1629
1706
|
body.channel = state.botChannel || 'telegram';
|
|
1630
1707
|
body.userTimezone = state.tz;
|
|
1631
1708
|
// Only the CREATE flow starts a Zalo QR login (the server kicks it off and sets
|
|
1632
|
-
// 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 -
|
|
1633
1710
|
// the session is already saved and the PUT endpoint starts no login.
|
|
1634
1711
|
if (body.channel === 'zalo-personal' && !state.botEditId) {
|
|
1635
1712
|
state.botModalOpen = false;
|
|
@@ -1643,7 +1720,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1643
1720
|
const url = isEdit ? `/api/bot/${encodeURIComponent(state.botEditId)}` : '/api/bot/create';
|
|
1644
1721
|
const method = isEdit ? 'PUT' : 'POST';
|
|
1645
1722
|
const result = await api(url, { method, body });
|
|
1646
|
-
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 : ''}`;
|
|
1647
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');
|
|
1648
1725
|
state.botChannel = body.channel;
|
|
1649
1726
|
state.botEditId = '';
|
|
@@ -1688,6 +1765,9 @@ async function loadStatus(silent=false){
|
|
|
1688
1765
|
state.install = install;
|
|
1689
1766
|
if (!state.selectedProjectDir && state.install?.projectDir) state.selectedProjectDir = state.install.projectDir;
|
|
1690
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();
|
|
1691
1771
|
}
|
|
1692
1772
|
function autoSwitchBotChannel() {
|
|
1693
1773
|
const bots = state.install?.bots || [];
|
|
@@ -1770,7 +1850,7 @@ function updateZaloHealthDom() {
|
|
|
1770
1850
|
});
|
|
1771
1851
|
}
|
|
1772
1852
|
// Live status: poll the health endpoint while Zalo bots are on screen (the server caches
|
|
1773
|
-
// 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 -
|
|
1774
1854
|
// no more pressing "Làm mới" to see a bot drop or come back.
|
|
1775
1855
|
const ZALO_HEALTH_POLL_MS = 10000;
|
|
1776
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}
|