create-openclaw-bot 5.13.3 → 5.13.4
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 +10 -2
- package/README.vi.md +10 -2
- package/dist/server/local-server.js +37 -7
- package/dist/setup/shared/bot-config-gen.js +5 -0
- package/dist/setup/shared/workspace-gen.js +23 -11
- package/dist/web/app.js +70 -6
- package/dist/web/styles.css +23 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*Run one command → open the dashboard → your bot is live. Windows · macOS · Linux · VPS — Docker-powered, auto-installed for you.*
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.13.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.13.4-0EA5E9?style=for-the-badge" alt="Version 5.13.4" /></a>
|
|
11
11
|
<a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
|
|
12
12
|
<a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
|
|
13
13
|
<a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
|
|
@@ -37,12 +37,20 @@
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
## 🆕 What's New in v5.13.
|
|
40
|
+
## 🆕 What's New in v5.13.4
|
|
41
|
+
|
|
42
|
+
- ⚙️ **New Settings page**: pick your theme (light/dark toggle), language (VI/EN) and timezone right from the dashboard — available in both the sidebar and the mobile nav bar.
|
|
43
|
+
- 🕒 **Schedules & cron run in the correct local time**: newly created bots carry an explicit timezone, so reminders/cron no longer drift a day off around midnight (a UTC-vs-local bug that could schedule into the past). Scheduling guidance was hardened end-to-end (local time + timezone, explicit delivery target, raw group id).
|
|
44
|
+
|
|
45
|
+
<details>
|
|
46
|
+
<summary><b>Previous: What's new in v5.13.0</b></summary>
|
|
41
47
|
|
|
42
48
|
- 🟢 **Live Zalo status on every bot card**: each personal Zalo bot now shows its own connection and login state, with a compact green badge when it is ready.
|
|
43
49
|
- 👥 **Accurate multi-account visibility**: Setup reads the real Zalo Connect runtime per account, so projects with several Zalo bots no longer collapse into an ambiguous shared status.
|
|
44
50
|
- ✨ **A cleaner Zalo control area**: Refresh and Login Again sit together above the bot list, while shared Zalo Connect and Zalo Mod versions move into the main status column.
|
|
45
51
|
|
|
52
|
+
</details>
|
|
53
|
+
|
|
46
54
|
<details>
|
|
47
55
|
<summary><b>Previous: What's new in v5.12.0</b></summary>
|
|
48
56
|
|
package/README.vi.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*Chạy một lệnh → mở dashboard → bot lên sóng. Windows · macOS · Linux · VPS — chạy trên Docker, tự cài giúp bạn.*
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.13.
|
|
10
|
+
<a href="https://github.com/tuanminhhole/openclaw-setup/releases"><img src="https://img.shields.io/badge/RELEASE-v5.13.4-0EA5E9?style=for-the-badge" alt="Version 5.13.4" /></a>
|
|
11
11
|
<a href="https://github.com/tuanminhhole/openclaw-setup?tab=MIT-1-ov-file"><img src="https://img.shields.io/badge/LICENSE-MIT-success?style=for-the-badge" alt="MIT License" /></a>
|
|
12
12
|
<a href="https://www.npmjs.com/package/create-openclaw-bot"><img src="https://img.shields.io/npm/v/create-openclaw-bot?style=for-the-badge&label=CLI&color=2563EB&logo=npm&logoColor=white" alt="NPM Version" /></a>
|
|
13
13
|
<a href="https://github.com/tuanminhhole/openclaw-setup/stargazers"><img src="https://img.shields.io/github/stars/tuanminhhole/openclaw-setup?style=for-the-badge&color=eab308&logo=github&logoColor=white" alt="GitHub Stars" /></a>
|
|
@@ -37,12 +37,20 @@
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
## 🆕 Có gì mới trong v5.13.
|
|
40
|
+
## 🆕 Có gì mới trong v5.13.4
|
|
41
|
+
|
|
42
|
+
- ⚙️ **Trang Cài đặt mới**: chọn giao diện (nút gạt Sáng/Tối), ngôn ngữ (VI/EN) và múi giờ ngay trong dashboard — có ở cả sidebar lẫn thanh điều hướng mobile.
|
|
43
|
+
- 🕒 **Lịch & cron chạy đúng giờ địa phương**: bot tạo mới nhận múi giờ rõ ràng, nên nhắc hẹn/cron không còn lệch 1 ngày quanh nửa đêm (lỗi UTC-vs-local có thể đặt lịch vào quá khứ). Đã siết hướng dẫn lập lịch toàn diện (giờ local + múi giờ, nơi gửi rõ ràng, ID nhóm dạng thô).
|
|
44
|
+
|
|
45
|
+
<details>
|
|
46
|
+
<summary><b>Trước đó: Có gì mới trong v5.13.0</b></summary>
|
|
41
47
|
|
|
42
48
|
- 🟢 **Trạng thái Zalo trực tiếp trên từng thẻ bot**: mỗi bot Zalo cá nhân hiển thị riêng trạng thái kết nối và đăng nhập, kèm badge xanh gọn khi đã sẵn sàng.
|
|
43
49
|
- 👥 **Nhận diện đa tài khoản chính xác**: Setup đọc runtime Zalo Connect theo từng tài khoản thật, nên project có nhiều bot Zalo không còn bị gom thành một trạng thái “chưa rõ”.
|
|
44
50
|
- ✨ **Khu điều khiển Zalo gọn hơn**: Làm mới và Đăng nhập lại nằm cùng hàng phía trên danh sách bot; phiên bản Zalo Connect và Zalo Mod được đưa sang cột trạng thái chung.
|
|
45
51
|
|
|
52
|
+
</details>
|
|
53
|
+
|
|
46
54
|
<details>
|
|
47
55
|
<summary><b>Trước đó: Có gì mới trong v5.12.0</b></summary>
|
|
48
56
|
|
|
@@ -1409,6 +1409,10 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1409
1409
|
const userDesc = String(body.userDescription || body.userDesc || '').trim();
|
|
1410
1410
|
const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
|
|
1411
1411
|
|
|
1412
|
+
// Múi giờ người dùng chọn ở UI Cài đặt (mặc định VN). Với project đã có sẵn, ưu tiên tz đã lưu
|
|
1413
|
+
// trong config để bot mới đồng bộ với các bot cũ.
|
|
1414
|
+
const bodyTz = String(body.userTimezone || '').trim() || 'Asia/Ho_Chi_Minh';
|
|
1415
|
+
|
|
1412
1416
|
const openclawHome = join(projectDir, '.openclaw');
|
|
1413
1417
|
await fsp.mkdir(openclawHome, { recursive: true });
|
|
1414
1418
|
const cfgPath = join(openclawHome, 'openclaw.json');
|
|
@@ -1421,7 +1425,9 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1421
1425
|
selectedSkills: ['memory', 'web-search', 'scheduler'],
|
|
1422
1426
|
skills: dataExport.SKILLS || [],
|
|
1423
1427
|
agentMetas: [],
|
|
1428
|
+
userTimezone: bodyTz,
|
|
1424
1429
|
}));
|
|
1430
|
+
const userTimezone = cfg.agents?.defaults?.userTimezone || bodyTz;
|
|
1425
1431
|
|
|
1426
1432
|
const used = new Set(cfg.agents.list.map((a) => a.id));
|
|
1427
1433
|
const botName = uniqueDisplayName(requestedBotName, new Set(cfg.agents.list.map((a) => a.name || a.id)));
|
|
@@ -1509,6 +1515,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
|
|
|
1509
1515
|
zaloBackend: zaloBackendForConfig(cfg),
|
|
1510
1516
|
hasScheduler,
|
|
1511
1517
|
hasImageGen,
|
|
1518
|
+
userTimezone,
|
|
1512
1519
|
});
|
|
1513
1520
|
const wsRoot = join(openclawHome, workspaceDir);
|
|
1514
1521
|
for (const [name, content] of Object.entries(files)) {
|
|
@@ -1631,6 +1638,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
|
|
|
1631
1638
|
zaloBackend: zaloBackendForConfig(cfg),
|
|
1632
1639
|
hasScheduler,
|
|
1633
1640
|
hasImageGen,
|
|
1641
|
+
userTimezone: cfg.agents?.defaults?.userTimezone || 'Asia/Ho_Chi_Minh',
|
|
1634
1642
|
});
|
|
1635
1643
|
const wsRoot = join(projectDir, '.openclaw', workspaceDir);
|
|
1636
1644
|
for (const [name, content] of Object.entries(files)) {
|
|
@@ -2326,7 +2334,7 @@ function json(res, data, status = 200) {
|
|
|
2326
2334
|
res.end(body);
|
|
2327
2335
|
}
|
|
2328
2336
|
|
|
2329
|
-
async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 18789, routerPort = 20128 }) {
|
|
2337
|
+
async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 18789, routerPort = 20128, userTimezone = 'Asia/Ho_Chi_Minh' }) {
|
|
2330
2338
|
await fsp.mkdir(projectDir, { recursive: true });
|
|
2331
2339
|
const openclawHome = join(projectDir, '.openclaw');
|
|
2332
2340
|
await fsp.mkdir(openclawHome, { recursive: true });
|
|
@@ -2334,7 +2342,7 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
|
|
|
2334
2342
|
|
|
2335
2343
|
const selectedSkills = ['memory', 'web-search', 'scheduler'];
|
|
2336
2344
|
const agentMetas = [];
|
|
2337
|
-
const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort };
|
|
2345
|
+
const common = { channelKey: 'telegram', providerKey: '9router', model: DEFAULT_MODEL, deployMode: mode, osChoice, selectedSkills, skills: dataExport.SKILLS || [], agentMetas, gatewayPort, routerPort, userTimezone };
|
|
2338
2346
|
const cfg = buildOpenclawJson(common);
|
|
2339
2347
|
// A core project has no channel account yet. Keep its environment shared/credential-free;
|
|
2340
2348
|
// writing the literal <your_bot_token> placeholder makes OpenClaw auto-detect Telegram and
|
|
@@ -2562,7 +2570,7 @@ async function ensureDockerInstalled(osChoice) {
|
|
|
2562
2570
|
throw httpError(400, 'Hệ điều hành không được hỗ trợ tự cài Docker. Hãy cài Docker thủ công rồi thử lại.');
|
|
2563
2571
|
}
|
|
2564
2572
|
|
|
2565
|
-
async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, routerPort = 20128 }) {
|
|
2573
|
+
async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, routerPort = 20128, userTimezone = 'Asia/Ho_Chi_Minh' }) {
|
|
2566
2574
|
state.installing = true;
|
|
2567
2575
|
state.installed = false;
|
|
2568
2576
|
state.lastError = null;
|
|
@@ -2576,7 +2584,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
|
|
|
2576
2584
|
// Make sure Docker is present (auto-install on Linux/VPS) before doing any work — fail fast
|
|
2577
2585
|
// with a clear message rather than deep inside `docker compose up`.
|
|
2578
2586
|
await ensureDockerInstalled(osChoice);
|
|
2579
|
-
await writeCoreProject({ projectDir, osChoice, mode, gatewayPort, routerPort });
|
|
2587
|
+
await writeCoreProject({ projectDir, osChoice, mode, gatewayPort, routerPort, userTimezone });
|
|
2580
2588
|
await run('npm', ['install', '-g', OPENCLAW_NPM_SPEC]);
|
|
2581
2589
|
await run('npm', ['install', '-g', NINE_ROUTER_NPM_SPEC]);
|
|
2582
2590
|
if (mode === 'docker') {
|
|
@@ -3781,6 +3789,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3781
3789
|
const body = await readJson(req);
|
|
3782
3790
|
const osChoice = body.os || detectOs();
|
|
3783
3791
|
const mode = body.mode || recommendedMode(osChoice);
|
|
3792
|
+
const userTimezone = String(body.userTimezone || '').trim() || 'Asia/Ho_Chi_Minh';
|
|
3784
3793
|
const projectDir = body.projectDir ? resolve(String(body.projectDir)) : resolve(rootProjectDir, body.projectName || DEFAULT_PROJECT_NAME);
|
|
3785
3794
|
|
|
3786
3795
|
// Auto-allocate unique, free ports to avoid collision (reserving gatewayPort + 1 for Zalo-mod UI)
|
|
@@ -3810,7 +3819,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3810
3819
|
state.gatewayUrl = `http://127.0.0.1:${gatewayPort}`;
|
|
3811
3820
|
state.routerUrl = `http://127.0.0.1:${routerPort}`;
|
|
3812
3821
|
|
|
3813
|
-
installCore({ osChoice, mode, projectDir, gatewayPort, routerPort }).catch(() => {});
|
|
3822
|
+
installCore({ osChoice, mode, projectDir, gatewayPort, routerPort, userTimezone }).catch(() => {});
|
|
3814
3823
|
state.projectDir = projectDir;
|
|
3815
3824
|
state.mode = mode;
|
|
3816
3825
|
state.os = osChoice;
|
|
@@ -4193,18 +4202,39 @@ function sshUserName() { try { return os.userInfo().username || 'root'; } catch
|
|
|
4193
4202
|
function isHeadlessServer() {
|
|
4194
4203
|
return process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
4195
4204
|
}
|
|
4205
|
+
// Kiểm tra nhanh 1 port có đang listen trên host không (tránh forward port chết).
|
|
4206
|
+
function isLocalPortListening(port, host = '127.0.0.1', timeout = 400) {
|
|
4207
|
+
return new Promise((resolve) => {
|
|
4208
|
+
const sock = net.connect({ port, host });
|
|
4209
|
+
let settled = false;
|
|
4210
|
+
const done = (v) => { if (!settled) { settled = true; try { sock.destroy(); } catch {} resolve(v); } };
|
|
4211
|
+
sock.setTimeout(timeout);
|
|
4212
|
+
sock.once('connect', () => done(true));
|
|
4213
|
+
sock.once('timeout', () => done(false));
|
|
4214
|
+
sock.once('error', () => done(false));
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4196
4217
|
// On a headless server there's no local browser — print an SSH-tunnel command so the
|
|
4197
4218
|
// operator can reach the dashboard AND the Open-web UIs from their own machine. This is
|
|
4198
4219
|
// the discoverable answer for ANY user on a VPS (no manual ssh-config knowledge needed).
|
|
4220
|
+
// CHỈ forward những port đang THỰC SỰ listen trên host — gateway (18789) / 9Router (20128)
|
|
4221
|
+
// thường nằm trong Docker, không bind ra host, nên nếu forward cứng sẽ đẻ ra hàng loạt
|
|
4222
|
+
// "channel: open failed: connect failed: Connection refused" vô nghĩa ở phía client.
|
|
4199
4223
|
async function printRemoteAccessHint(uiPort) {
|
|
4200
4224
|
if (!isHeadlessServer()) return;
|
|
4201
4225
|
const ip = (await getPublicIp()) || '<your-server-ip>';
|
|
4202
|
-
|
|
4226
|
+
// uiPort vừa bind xong nên chắc chắn mở; các port phụ chỉ thêm nếu đang listen.
|
|
4227
|
+
const extras = [];
|
|
4228
|
+
for (const p of [18789, 20128, 18790]) {
|
|
4229
|
+
if (p !== uiPort && await isLocalPortListening(p)) extras.push(p);
|
|
4230
|
+
}
|
|
4231
|
+
const ports = [uiPort, ...extras];
|
|
4232
|
+
const fwd = ports.map((p) => `-L ${p}:127.0.0.1:${p}`).join(' ');
|
|
4203
4233
|
console.log('');
|
|
4204
4234
|
console.log('🌐 No local browser detected (server/VPS). Open the UI from YOUR computer:');
|
|
4205
4235
|
console.log(` ssh ${fwd} ${sshUserName()}@${ip}`);
|
|
4206
4236
|
console.log(` then open: http://localhost:${uiPort}`);
|
|
4207
|
-
console.log(
|
|
4237
|
+
console.log(` (forwards ${ports.length} live port${ports.length > 1 ? 's' : ''}: ${ports.join(', ')})`);
|
|
4208
4238
|
console.log('');
|
|
4209
4239
|
}
|
|
4210
4240
|
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
osChoice = '',
|
|
76
76
|
selectedModel = '',
|
|
77
77
|
routerPort,
|
|
78
|
+
userTimezone = 'Asia/Ho_Chi_Minh',
|
|
78
79
|
} = opts;
|
|
79
80
|
|
|
80
81
|
const common = _common;
|
|
@@ -108,6 +109,10 @@
|
|
|
108
109
|
agents: {
|
|
109
110
|
defaults: {
|
|
110
111
|
model: { primary: model, fallbacks: [] },
|
|
112
|
+
// Giờ địa phương agent nhìn thấy trong prompt ("Current time: … (tz)"). Nếu để trống,
|
|
113
|
+
// OpenClaw fallback UTC → agent hiểu "hôm nay/tối nay" theo ngày UTC, lệch 1 ngày quanh
|
|
114
|
+
// nửa đêm và đặt cron/lịch vào quá khứ. Lấy tz người dùng chọn ở UI Cài đặt (mặc định VN).
|
|
115
|
+
userTimezone,
|
|
111
116
|
compaction: { mode: 'safeguard' },
|
|
112
117
|
// Trim stale tool results before Anthropic's prompt-cache TTL expires so the
|
|
113
118
|
// re-cache write stays small → lower token cost on long sessions, zero downside.
|
|
@@ -547,7 +547,7 @@ _Good luck out there. Make it count._
|
|
|
547
547
|
- [Operating Manual](./AGENTS.md)`;
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
-
function buildCronjobSkillMd(isVi = true, zaloDeliveryChannel = 'zalo-connect') {
|
|
550
|
+
function buildCronjobSkillMd(isVi = true, zaloDeliveryChannel = 'zalo-connect', tz = 'Asia/Ho_Chi_Minh') {
|
|
551
551
|
return `---
|
|
552
552
|
name: cronjob
|
|
553
553
|
description: Lên lịch tác vụ định kỳ sử dụng công cụ cron.
|
|
@@ -562,18 +562,29 @@ Truyền tham số \`job\` (object) gồm:
|
|
|
562
562
|
- **\`agentId\`**: Bỏ qua (không truyền). Hệ thống tự gán.
|
|
563
563
|
- **\`sessionTarget\`**: \`"isolated"\` (cho chạy nền) hoặc \`"main"\`.
|
|
564
564
|
- **\`wakeMode\`**: \`"now"\`.
|
|
565
|
-
- **\`schedule
|
|
566
|
-
- \`kind
|
|
567
|
-
- \`
|
|
568
|
-
|
|
565
|
+
- **\`schedule\`** — chọn 1 trong 2 loại:
|
|
566
|
+
- **Lặp lại:** \`kind: "cron"\`, \`expr\`: biểu thức cron 5 trường (ví dụ \`"30 7 * * *"\` = 7h30 mỗi ngày), \`tz\`: múi giờ (BẮT BUỘC, ví dụ \`"${tz}"\`).
|
|
567
|
+
- **Một lần:** \`kind: "at"\`, \`at\`: giờ địa phương + \`tz\` (khuyên dùng), ví dụ \`"at": "2026-07-22T00:30:00", "tz": "${tz}"\`. Cũng có thể dùng \`+duration\` (vd \`"+30m"\`). **KHÔNG tự quy đổi & ghi \`...Z\` (UTC)** — dễ sai ngày.
|
|
568
|
+
- 🕒 **XÁC ĐỊNH ĐÚNG NGÀY:** "Current time" trong ngữ cảnh đã theo giờ địa phương (\`${tz}\`). Cứ dùng NGÀY đang thấy để suy "hôm nay / tối nay / sáng mai". (Nếu vô tình thấy giờ dạng \`...Z\` UTC thì phải đổi sang \`${tz}\` trước khi suy ngày.)
|
|
569
|
+
- ✅ Nếu tool báo \`schedule.at is in the past\` → tính sai ngày (thường thiếu 1 ngày). Cộng thêm 1 ngày rồi thử lại; đừng báo lỗi khi chưa tự sửa.
|
|
569
570
|
- **\`payload\`**:
|
|
570
571
|
- \`kind\`: \`"agentTurn"\`.
|
|
571
|
-
- \`message\`:
|
|
572
|
-
- **\`delivery
|
|
572
|
+
- \`message\`: **NỘI DUNG Y NGUYÊN bot sẽ nói ra** (câu khẳng định), vd \`"Chúc cả nhà ngủ ngon 🌙"\`. ⚠️ KHÔNG viết kiểu mệnh lệnh ("gửi lời chúc tới group ..."). Khi job chạy, \`delivery.announce\` sẽ TỰ gửi nội dung này tới \`to\`. **TUYỆT ĐỐI KHÔNG để job tự gọi tool gửi tin (message/send) — announce đã lo, tự gọi sẽ gây lỗi \`Unknown target\` và "Cron failed".**
|
|
573
|
+
- **\`delivery\`** — 🚨 **BẮT BUỘC điền ĐỦ cả \`channel\` VÀ \`to\`.** Nếu thiếu (hoặc để \`channel: "last"\`), job vẫn tạo được nhưng khi tới giờ sẽ **FAIL lúc giao** với lỗi \`Refusing implicit isolated cron delivery\` → tin KHÔNG tới nhóm:
|
|
573
574
|
- \`mode\`: \`"announce"\`.
|
|
574
|
-
- \`channel\`: \`"${zaloDeliveryChannel}"
|
|
575
|
-
- \`to\`:
|
|
576
|
-
|
|
575
|
+
- \`channel\`: \`"${zaloDeliveryChannel}"\` (KHÔNG để \`"last"\`).
|
|
576
|
+
- \`to\`: nơi nhận = **ID THÔ (threadId), KHÔNG thêm tiền tố gì**. Group Zalo → dùng thẳng groupId (vd \`"1925989252066183028"\`). DM cá nhân → dùng userId. ⚠️ **TUYỆT ĐỐI KHÔNG thêm \`g:\`** — nếu thêm, zalo-connect báo \`Unknown target "g:..."\` và tin KHÔNG gửi được.
|
|
577
|
+
|
|
578
|
+
### 📦 Ví dụ job ĐẦY ĐỦ (một lần, giờ địa phương) — tạo RIÊNG 1 job cho MỖI group:
|
|
579
|
+
\`\`\`json
|
|
580
|
+
{
|
|
581
|
+
"sessionTarget": "isolated",
|
|
582
|
+
"wakeMode": "now",
|
|
583
|
+
"schedule": { "kind": "at", "at": "2026-07-22T00:30:00", "tz": "${tz}" },
|
|
584
|
+
"payload": { "kind": "agentTurn", "message": "Chúc cả nhà ngủ ngon 🌙" },
|
|
585
|
+
"delivery": { "mode": "announce", "channel": "${zaloDeliveryChannel}", "to": "1925989252066183028" }
|
|
586
|
+
}
|
|
587
|
+
\`\`\`
|
|
577
588
|
|
|
578
589
|
## 2. Tìm kiếm, Tắt, Bật, Xóa Lịch
|
|
579
590
|
- **Xem danh sách:** Gọi \`cron\` với action \`list\` (kèm \`includeDisabled: true\`). Tìm \`id\` phù hợp.
|
|
@@ -1304,6 +1315,7 @@ Add whatever helps you do your job. This is your cheat sheet.
|
|
|
1304
1315
|
hasImageGen = false,
|
|
1305
1316
|
hasZaloMod = false,
|
|
1306
1317
|
zaloBackend = 'zalo-connect',
|
|
1318
|
+
userTimezone = 'Asia/Ho_Chi_Minh',
|
|
1307
1319
|
} = opts;
|
|
1308
1320
|
|
|
1309
1321
|
const isMultiBot = variant === 'relay';
|
|
@@ -1330,7 +1342,7 @@ Add whatever helps you do your job. This is your cheat sheet.
|
|
|
1330
1342
|
}
|
|
1331
1343
|
|
|
1332
1344
|
if (hasScheduler) {
|
|
1333
|
-
files['skills/cronjob/SKILL.md'] = buildCronjobSkillMd(isVi, 'zalo-connect');
|
|
1345
|
+
files['skills/cronjob/SKILL.md'] = buildCronjobSkillMd(isVi, 'zalo-connect', userTimezone);
|
|
1334
1346
|
}
|
|
1335
1347
|
|
|
1336
1348
|
if (hasImageGen) {
|
package/dist/web/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const $ = (sel) => document.querySelector(sel);
|
|
2
|
-
const state = { tab: 'dashboard', system: null, install: null, files: [], catalog: { skills: [], plugins: [] }, logs: [], zaloLoginOpen: false, zaloLoginLines: [], zaloQrDataUrl: '', lang: localStorage.getItem('openclaw-lang') || 'vi', theme: localStorage.getItem('openclaw-theme') || 'dark', navCollapsed: localStorage.getItem('openclaw-nav')==='1', os: null, mode: null, donateOpen: false, botModalOpen: false, botEditId: '', installModalOpen: false, fbPluginModalOpen: false, installTab: 'docker', installDraft: null, pathModal: null, confirmModal: null, botChannel: 'telegram', botPane: 'list', activeBotId: '', selectedFile: '', botMessage: '', projectConnectMessage: '', pendingProjectDir: '', selectedProjectDir: '', featureFlags: {}, featureInstalled: {}, featureLoading: {}, zaloBackend: '', zaloHealth: null, openDirs: {} };
|
|
2
|
+
const state = { tab: 'dashboard', system: null, install: null, files: [], catalog: { skills: [], plugins: [] }, logs: [], zaloLoginOpen: false, zaloLoginLines: [], zaloQrDataUrl: '', lang: localStorage.getItem('openclaw-lang') || 'vi', theme: localStorage.getItem('openclaw-theme') || 'dark', tz: localStorage.getItem('openclaw-tz') || 'Asia/Ho_Chi_Minh', navCollapsed: localStorage.getItem('openclaw-nav')==='1', os: null, mode: null, donateOpen: false, botModalOpen: false, botEditId: '', installModalOpen: false, fbPluginModalOpen: false, installTab: 'docker', installDraft: null, pathModal: null, confirmModal: null, botChannel: 'telegram', botPane: 'list', activeBotId: '', selectedFile: '', botMessage: '', projectConnectMessage: '', pendingProjectDir: '', selectedProjectDir: '', featureFlags: {}, featureInstalled: {}, featureLoading: {}, zaloBackend: '', zaloHealth: null, openDirs: {} };
|
|
3
3
|
const SVG_CDN = 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons';
|
|
4
4
|
const OS_OPTIONS = [
|
|
5
5
|
{ id: 'win', title: 'Windows', subtitle: 'Auto-detected desktop', icon: `${SVG_CDN}/windows/default.svg`, badge: 'Desktop' },
|
|
@@ -86,7 +86,8 @@ function icon(name) {
|
|
|
86
86
|
bot: 'M7 8h10v8H7z M9 4h6v4H9z M9 11h.01M15 11h.01M10 16h4',
|
|
87
87
|
files: 'M6 3h8l4 4v14H6z M14 3v5h5 M9 13h6 M9 17h6',
|
|
88
88
|
skills: 'M12 2l2.4 6.8H22l-6 4.4 2.3 6.8-6.3-4.2L5.7 20 8 13.2 2 8.8h7.6z',
|
|
89
|
-
logs: 'M4 6h16M4 12h16M4 18h10'
|
|
89
|
+
logs: 'M4 6h16M4 12h16M4 18h10',
|
|
90
|
+
settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z M19.4 13a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-2.9 1.2V21a2 2 0 1 1-4 0v-.1A1.7 1.7 0 0 0 7 19.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0-1.2-2.9H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.7 7l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9.5A1.7 1.7 0 0 0 10.5 3V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 2.9 1.2l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9v.1a1.7 1.7 0 0 0 1.6 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z'
|
|
90
91
|
};
|
|
91
92
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="${paths[name]}"/></svg>`;
|
|
92
93
|
}
|
|
@@ -247,7 +248,10 @@ function ui(key) {
|
|
|
247
248
|
next:['Ti\u1ebfp theo','Next'], nextDesc:['S\u1eeda file nh\u1eadn di\u1ec7n, sau \u0111\u00f3 c\u00e0i k\u1ef9 n\u0103ng/plugin.','Edit identity files, then install skills/plugins.'], openFiles:['M\u1edf t\u1ec7p','Open files'],
|
|
248
249
|
save:['L\u01b0u','Save'], saved:['\u0110\u00e3 l\u01b0u','Saved'], noFiles:['Ch\u01b0a c\u00f3 file markdown. H\u00e3y ch\u1ea1y c\u00e0i \u0111\u1eb7t tr\u01b0\u1edbc.','No markdown files yet. Run install first.'],
|
|
249
250
|
nativeCap:['N\u0103ng l\u1ef1c native','native capability'], plugins:['Plugins','Plugins'], installVerb:['C\u00e0i \u0111\u1eb7t','Install'],
|
|
250
|
-
desktop:['M\u00e1y b\u00e0n','Desktop'], server:['M\u00e1y ch\u1ee7','Server'], recommended:['Khuy\u00ean d\u00f9ng','Recommended'], advanced:['N\u00e2ng cao','Advanced']
|
|
251
|
+
desktop:['M\u00e1y b\u00e0n','Desktop'], server:['M\u00e1y ch\u1ee7','Server'], recommended:['Khuy\u00ean d\u00f9ng','Recommended'], advanced:['N\u00e2ng cao','Advanced'],
|
|
252
|
+
settings:['T\u00f9y ch\u1ec9nh','Settings'], appearance:['Giao di\u1ec7n','Appearance'], darkMode:['Ch\u1ebf \u0111\u1ed9 t\u1ed1i','Dark mode'], language:['Ng\u00f4n ng\u1eef','Language'], timezone:['M\u00fai gi\u1edd','Timezone'],
|
|
253
|
+
settingsDesc:['T\u00f9y ch\u1ec9nh giao di\u1ec7n, ng\u00f4n ng\u1eef v\u00e0 m\u00fai gi\u1edd. M\u00fai gi\u1edd \u0111\u01b0\u1ee3c d\u00f9ng cho bot t\u1ea1o m\u1edbi (l\u1ecbch/cron ch\u1ea1y \u0111\u00fang gi\u1edd \u0111\u1ecba ph\u01b0\u01a1ng).','Customize appearance, language and timezone. The timezone is applied to newly created bots (so schedules/cron run in local time).'],
|
|
254
|
+
tzHint:['\u00c1p d\u1ee5ng cho bot t\u1ea1o m\u1edbi','Applied to newly created bots']
|
|
251
255
|
}[key] || [key,key];
|
|
252
256
|
return t(m[0], m[1]);
|
|
253
257
|
}
|
|
@@ -408,7 +412,7 @@ function render() {
|
|
|
408
412
|
// Expose the active tab on <body> so CSS can drop the redundant page-title header on the
|
|
409
413
|
// dashboard (the dashboard hero already carries its own title).
|
|
410
414
|
document.body.dataset.activeTab = state.tab;
|
|
411
|
-
const tabs = [['dashboard',t('Dashboard','Dashboard')],['setup',ui('setup')],['bot',ui('bot')],['logs',ui('logs')]];
|
|
415
|
+
const tabs = [['dashboard',t('Dashboard','Dashboard')],['setup',ui('setup')],['bot',ui('bot')],['logs',ui('logs')],['settings',ui('settings')]];
|
|
412
416
|
|
|
413
417
|
let mainContainer = $('#app-main-content');
|
|
414
418
|
if (!mainContainer) {
|
|
@@ -595,16 +599,66 @@ function wireSkillsHandlers(scope = document) {
|
|
|
595
599
|
}
|
|
596
600
|
|
|
597
601
|
function title() {
|
|
598
|
-
return { dashboard: t('Dashboard vận hành','Operations dashboard'), setup: t('C\u00e0i OpenClaw trong v\u00e0i ph\u00fat', 'Install OpenClaw in minutes'), bot: t('B\u1ea3ng \u0111i\u1ec1u khi\u1ec3n bot','Bot dashboard'), skills: t('K\u1ef9 n\u0103ng & plugins','Skills & plugins'), logs: t('Nh\u1eadt k\u00fd c\u00e0i \u0111\u1eb7t','Install logs') }[state.tab];
|
|
602
|
+
return { dashboard: t('Dashboard vận hành','Operations dashboard'), setup: t('C\u00e0i OpenClaw trong v\u00e0i ph\u00fat', 'Install OpenClaw in minutes'), bot: t('B\u1ea3ng \u0111i\u1ec1u khi\u1ec3n bot','Bot dashboard'), skills: t('K\u1ef9 n\u0103ng & plugins','Skills & plugins'), logs: t('Nh\u1eadt k\u00fd c\u00e0i \u0111\u1eb7t','Install logs'), settings: ui('settings') }[state.tab] || '';
|
|
599
603
|
}
|
|
600
604
|
|
|
601
605
|
function content() {
|
|
602
606
|
if (state.tab === 'dashboard') return dashboardView();
|
|
603
607
|
if (state.tab === 'setup') return setupView();
|
|
604
608
|
if (state.tab === 'bot') return botView();
|
|
609
|
+
if (state.tab === 'settings') return settingsView();
|
|
605
610
|
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>`;
|
|
606
611
|
}
|
|
607
612
|
|
|
613
|
+
// Danh sách múi giờ phổ biến (IANA) cho dropdown pill. Gọn — đủ dùng cho khu vực chính.
|
|
614
|
+
const TZ_OPTIONS = [
|
|
615
|
+
['Asia/Ho_Chi_Minh', 'Việt Nam (UTC+7)'],
|
|
616
|
+
['Asia/Bangkok', 'Bangkok (UTC+7)'],
|
|
617
|
+
['Asia/Jakarta', 'Jakarta (UTC+7)'],
|
|
618
|
+
['Asia/Singapore', 'Singapore (UTC+8)'],
|
|
619
|
+
['Asia/Kuala_Lumpur', 'Kuala Lumpur (UTC+8)'],
|
|
620
|
+
['Asia/Manila', 'Manila (UTC+8)'],
|
|
621
|
+
['Asia/Shanghai', 'Trung Quốc (UTC+8)'],
|
|
622
|
+
['Asia/Tokyo', 'Tokyo (UTC+9)'],
|
|
623
|
+
['Asia/Seoul', 'Seoul (UTC+9)'],
|
|
624
|
+
['Asia/Kolkata', 'Ấn Độ (UTC+5:30)'],
|
|
625
|
+
['Asia/Dubai', 'Dubai (UTC+4)'],
|
|
626
|
+
['Australia/Sydney', 'Sydney (UTC+10/+11)'],
|
|
627
|
+
['Europe/London', 'London (UTC+0/+1)'],
|
|
628
|
+
['Europe/Paris', 'Paris (UTC+1/+2)'],
|
|
629
|
+
['America/New_York', 'New York (UTC-5/-4)'],
|
|
630
|
+
['America/Los_Angeles', 'Los Angeles (UTC-8/-7)'],
|
|
631
|
+
['UTC', 'UTC'],
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
function settingsView() {
|
|
635
|
+
const tzKnown = TZ_OPTIONS.some(([id]) => id === state.tz);
|
|
636
|
+
const tzOpts = (tzKnown ? TZ_OPTIONS : [[state.tz, state.tz], ...TZ_OPTIONS])
|
|
637
|
+
.map(([id, label]) => `<option value="${id}" ${state.tz === id ? 'selected' : ''}>${escapeHtml(label)}</option>`).join('');
|
|
638
|
+
return `<div class="panel settings-panel">
|
|
639
|
+
<p class="lead" style="margin-bottom:22px">${ui('settingsDesc')}</p>
|
|
640
|
+
<div class="grid" style="max-width:640px">
|
|
641
|
+
<div class="card set-row">
|
|
642
|
+
<div class="set-row__label"><b>${ui('appearance')}</b><span>${t('Bật để dùng nền tối','Toggle for dark background')}</span></div>
|
|
643
|
+
<label class="switch" title="${ui('darkMode')}">
|
|
644
|
+
<input type="checkbox" data-pref-switch="theme" ${state.theme === 'dark' ? 'checked' : ''} aria-label="${ui('darkMode')}">
|
|
645
|
+
<span class="switch__track"><span class="switch__knob"></span></span>
|
|
646
|
+
</label>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="card set-row">
|
|
649
|
+
<div class="set-row__label"><b>${ui('language')}</b><span>${t('Ngôn ngữ hiển thị của trình cài đặt','Installer display language')}</span></div>
|
|
650
|
+
${toggleGroup('lang', state.lang, [['vi','VI'],['en','EN']])}
|
|
651
|
+
</div>
|
|
652
|
+
<div class="card set-row">
|
|
653
|
+
<div class="set-row__label"><b>${ui('timezone')}</b><span>${ui('tzHint')}</span></div>
|
|
654
|
+
<div class="tz-select"><select id="tz-select" aria-label="${ui('timezone')}">${tzOpts}</select>
|
|
655
|
+
<svg class="tz-select__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
</div>`;
|
|
660
|
+
}
|
|
661
|
+
|
|
608
662
|
function dashboardView() {
|
|
609
663
|
const s = state.install || {};
|
|
610
664
|
const sys = state.system || {};
|
|
@@ -1126,8 +1180,16 @@ function wireTab() {
|
|
|
1126
1180
|
localStorage.setItem('openclaw-'+btn.dataset.pref, btn.dataset.value);
|
|
1127
1181
|
const main = $('#app-main-content');
|
|
1128
1182
|
if (main) main.remove();
|
|
1129
|
-
render();
|
|
1183
|
+
render();
|
|
1184
|
+
});
|
|
1185
|
+
document.querySelectorAll('[data-pref-switch]').forEach(sw => sw.onchange = () => {
|
|
1186
|
+
const pref = sw.dataset.prefSwitch;
|
|
1187
|
+
const val = pref === 'theme' ? (sw.checked ? 'dark' : 'light') : (sw.checked ? 'on' : 'off');
|
|
1188
|
+
state[pref] = val;
|
|
1189
|
+
localStorage.setItem('openclaw-' + pref, val);
|
|
1190
|
+
applyPrefs();
|
|
1130
1191
|
});
|
|
1192
|
+
{ const tzSel = $('#tz-select'); if (tzSel) tzSel.onchange = () => { state.tz = tzSel.value; localStorage.setItem('openclaw-tz', tzSel.value); }; }
|
|
1131
1193
|
document.querySelectorAll('[data-tab-jump]').forEach(btn => btn.onclick = () => { state.tab = btn.dataset.tabJump; render(); });
|
|
1132
1194
|
document.querySelectorAll('input[name=os]').forEach(i => i.onchange = () => { state.os = i.value; document.querySelectorAll('input[name=os]').forEach(x => x.closest('.choice-card')?.classList.toggle('is-selected', x.checked)); });
|
|
1133
1195
|
document.querySelectorAll('input[name=mode]').forEach(i => i.onchange = () => { state.mode = i.value; document.querySelectorAll('input[name=mode]').forEach(x => x.closest('.choice-card')?.classList.toggle('is-selected', x.checked)); });
|
|
@@ -1393,6 +1455,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1393
1455
|
os: form.querySelector('[name="os"]')?.value || state.installDraft?.os || state.os,
|
|
1394
1456
|
mode: form.querySelector('[name="mode"]')?.value || state.installTab || state.installDraft?.mode || state.mode,
|
|
1395
1457
|
projectDir: form.querySelector('[name=\"projectDir\"]')?.value || '',
|
|
1458
|
+
userTimezone: state.tz,
|
|
1396
1459
|
};
|
|
1397
1460
|
if (!body.projectDir) throw new Error(t('Chưa có đường dẫn project','Missing project path'));
|
|
1398
1461
|
await api('/api/install', { method: 'POST', body });
|
|
@@ -1429,6 +1492,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1429
1492
|
const body = Object.fromEntries(fd.entries());
|
|
1430
1493
|
body.projectDir = activeProjectDir();
|
|
1431
1494
|
body.channel = state.botChannel || 'telegram';
|
|
1495
|
+
body.userTimezone = state.tz;
|
|
1432
1496
|
if (body.channel === 'zalo-personal') {
|
|
1433
1497
|
state.botModalOpen = false;
|
|
1434
1498
|
state.zaloLoginOpen = true;
|
package/dist/web/styles.css
CHANGED
|
@@ -112,6 +112,27 @@ html[data-theme="light"] .install-tab{background:#fff;border-color:#e7d9dd;color
|
|
|
112
112
|
html[data-theme="light"] .install-tab.is-active{background:linear-gradient(135deg,rgba(255,36,54,.12),rgba(255,90,102,.05));border-color:rgba(255,36,54,.55)}
|
|
113
113
|
html[data-theme="light"] .pill-choice{background:#fff;border-color:#e7d9dd;color:#1f1114}
|
|
114
114
|
html[data-theme="light"] .pill-choice.is-active{background:linear-gradient(135deg,rgba(255,36,54,.12),rgba(255,90,102,.05));border-color:rgba(255,36,54,.55)}
|
|
115
|
+
/* ── Settings panel: rows + toggle switch + pill timezone dropdown ── */
|
|
116
|
+
.settings-panel .set-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 18px}
|
|
117
|
+
.set-row__label{display:flex;flex-direction:column;gap:3px;min-width:0}
|
|
118
|
+
.set-row__label b{font-size:14px}
|
|
119
|
+
.set-row__label span{color:var(--muted);font-size:12px}
|
|
120
|
+
.switch{position:relative;display:inline-flex;align-items:center;cursor:pointer;flex:none}
|
|
121
|
+
.switch input{position:absolute;opacity:0;width:0;height:0}
|
|
122
|
+
.switch__track{width:46px;height:26px;border-radius:999px;background:var(--surface2);border:1px solid var(--hair);transition:background .2s,border-color .2s;display:inline-flex;align-items:center;padding:2px}
|
|
123
|
+
.switch__knob{width:20px;height:20px;border-radius:50%;background:var(--muted);transition:transform .2s,background .2s;transform:translateX(0)}
|
|
124
|
+
.switch input:checked + .switch__track{background:linear-gradient(135deg,var(--primary),var(--soft));border-color:var(--primary)}
|
|
125
|
+
.switch input:checked + .switch__track .switch__knob{transform:translateX(20px);background:#fff}
|
|
126
|
+
.switch input:focus-visible + .switch__track{box-shadow:0 0 0 3px rgba(255,36,54,.3)}
|
|
127
|
+
.tz-select{position:relative;display:inline-flex;align-items:center;flex:none}
|
|
128
|
+
.tz-select select{appearance:none;-webkit-appearance:none;border:1px solid var(--hair);background:var(--surface2);color:var(--ink);border-radius:999px;padding:9px 38px 9px 16px;font-size:13px;font-weight:600;cursor:pointer;min-width:210px;transition:border-color .2s,box-shadow .2s}
|
|
129
|
+
.tz-select select:hover{border-color:var(--soft)}
|
|
130
|
+
.tz-select select:focus{outline:0;border-color:var(--primary);box-shadow:0 0 0 3px rgba(255,36,54,.18)}
|
|
131
|
+
.tz-select__chevron{position:absolute;right:13px;width:16px;height:16px;pointer-events:none;color:var(--muted)}
|
|
132
|
+
html[data-theme="light"] .tz-select select{background:#fff;border-color:#e7d9dd;color:#1f1114}
|
|
133
|
+
html[data-theme="light"] .switch__track{background:#ece3e5;border-color:#e0d2d5}
|
|
134
|
+
html[data-theme="light"] .switch__knob{background:#b9adb0}
|
|
135
|
+
@media(max-width:760px){.settings-panel .set-row{gap:12px}.tz-select select{min-width:150px}}
|
|
115
136
|
@media(max-width:1180px){.sidebar-extra{display:none}}@media(max-width:720px){.donate-grid{grid-template-columns:1fr}.donate-modal{padding:16px;border-radius:18px}.donate-head h2{font-size:26px}}
|
|
116
137
|
|
|
117
138
|
|
|
@@ -1913,8 +1934,8 @@ main{transition:margin .2s}
|
|
|
1913
1934
|
.channel-tabs button.is-coming-soon{opacity:.5;cursor:not-allowed}
|
|
1914
1935
|
.channel-tabs::-webkit-scrollbar{height:6px}
|
|
1915
1936
|
.channel-tabs::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:999px}
|
|
1916
|
-
/* Bottom "native" nav bar:
|
|
1917
|
-
.bottom{grid-template-columns:repeat(
|
|
1937
|
+
/* Bottom "native" nav bar: 5 items (Dashboard, Cài đặt, Bot, Nhật ký, Tùy chỉnh) */
|
|
1938
|
+
.bottom{grid-template-columns:repeat(5,1fr)!important}
|
|
1918
1939
|
/* Tablet/mobile bot page: stop horizontal overflow so .channel-tabs scrolls instead of stretching the layout */
|
|
1919
1940
|
.bot-page,.bot-top-row,.bot-main,.bot-main-content,.bot-projects-and-bots,.bot-side-col,.bot-list{min-width:0}
|
|
1920
1941
|
@media(max-width:1180px){.bot-top-row{grid-template-columns:minmax(0,1fr)}}
|