create-openclaw-bot 5.13.3 → 5.13.5
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 +88 -9
- package/dist/setup/shared/bot-config-gen.js +7 -0
- package/dist/setup/shared/common-gen.js +2 -2
- package/dist/setup/shared/workspace-gen.js +89 -14
- package/dist/web/app.js +75 -8
- 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.5-0EA5E9?style=for-the-badge" alt="Version 5.13.5" /></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.5
|
|
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.5-0EA5E9?style=for-the-badge" alt="Version 5.13.5" /></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.5
|
|
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') {
|
|
@@ -3065,7 +3073,12 @@ async function applyFeatureToggle(projectDir, agentId, kind, id, enabled) {
|
|
|
3065
3073
|
|
|
3066
3074
|
const k = `${kind}:${id}`;
|
|
3067
3075
|
|
|
3068
|
-
|
|
3076
|
+
// zalo-connect is required by any Zalo bot — refuse to disable it while a Zalo binding exists
|
|
3077
|
+
// (the UI also locks the toggle; this is the backend guard).
|
|
3078
|
+
if (kind === 'plugin' && (id === 'zalo-connect' || id === 'openclaw-zalo-connect') && !enabled) {
|
|
3079
|
+
const hasZaloBot = (cfg.bindings || []).some((b) => b?.match?.channel === 'zalo-connect');
|
|
3080
|
+
if (hasZaloBot) throw httpError(400, 'Không thể tắt Zalo Connect khi vẫn còn bot Zalo.');
|
|
3081
|
+
}
|
|
3069
3082
|
|
|
3070
3083
|
if (kind === 'skill' && id === 'cron') {
|
|
3071
3084
|
cfg.skills = cfg.skills || { entries: {} };
|
|
@@ -3330,6 +3343,41 @@ async function installFeature(projectDir, agentId, kind, id) {
|
|
|
3330
3343
|
}
|
|
3331
3344
|
|
|
3332
3345
|
if (kind === 'plugin') {
|
|
3346
|
+
// zalo-connect is a GIT-pinned fork (not a ClawHub package) — install/update it by cloning the
|
|
3347
|
+
// pinned tag inside the container (same as first-boot/login). Powers the dashboard "Update"
|
|
3348
|
+
// button so existing Zalo bots can upgrade to the pinned version (bump the pin in common-gen.js).
|
|
3349
|
+
if (id === 'zalo-connect' || id === 'openclaw-zalo-connect') {
|
|
3350
|
+
let composeDir = null;
|
|
3351
|
+
if (existsSync(join(projectDir, 'docker-compose.yml'))) composeDir = projectDir;
|
|
3352
|
+
else if (existsSync(join(projectDir, 'docker', 'openclaw', 'docker-compose.yml'))) composeDir = join(projectDir, 'docker', 'openclaw');
|
|
3353
|
+
const repo = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[0];
|
|
3354
|
+
const ref = String(ZALO_CONNECT_PLUGIN_SPEC).split('#')[1] || ZALO_CONNECT_VERSION;
|
|
3355
|
+
if (composeDir) {
|
|
3356
|
+
const botContainer = getBotContainerName(projectDir);
|
|
3357
|
+
sendLog(`[zalo-connect] Installing/updating pinned ${ref} inside ${botContainer}...`);
|
|
3358
|
+
const cmd = `tmp=/tmp/openclaw-plugin-zalo-connect-${ref}; rm -rf "$tmp"; git clone --depth 1 --branch "${ref}" "${repo}" "$tmp" && rm -rf "$tmp/.git" && cd /home/node/project && openclaw plugins install "$tmp" --force 2>&1`;
|
|
3359
|
+
const out = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
|
|
3360
|
+
if (out) for (const line of `${out.stdout}\n${out.stderr}`.split(/\r?\n/).filter(Boolean)) sendLog(`[zalo-connect] ${line}`);
|
|
3361
|
+
const okDir = existsSync(join(projectDir, '.openclaw', 'extensions', 'zalo-connect'));
|
|
3362
|
+
if (out.code !== 0 && !okDir) throw new Error(out.stderr || out.stdout || 'Failed to install zalo-connect.');
|
|
3363
|
+
}
|
|
3364
|
+
const cfgPath = join(projectDir, '.openclaw', 'openclaw.json');
|
|
3365
|
+
if (existsSync(cfgPath)) {
|
|
3366
|
+
const cfg = ensureConfigShape(JSON.parse(await fsp.readFile(cfgPath, 'utf8')));
|
|
3367
|
+
cfg.plugins = cfg.plugins || { entries: {}, allow: [] };
|
|
3368
|
+
cfg.plugins.entries = cfg.plugins.entries || {};
|
|
3369
|
+
cfg.plugins.entries['zalo-connect'] = cfg.plugins.entries['zalo-connect'] || {};
|
|
3370
|
+
cfg.plugins.entries['zalo-connect'].enabled = true;
|
|
3371
|
+
cfg.plugins.allow = cfg.plugins.allow || [];
|
|
3372
|
+
if (!cfg.plugins.allow.includes('zalo-connect')) cfg.plugins.allow.push('zalo-connect');
|
|
3373
|
+
await fsp.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
3374
|
+
}
|
|
3375
|
+
if (composeDir) {
|
|
3376
|
+
sendLog('[zalo-connect] Restarting container to apply...');
|
|
3377
|
+
await run('docker', ['restart', getBotContainerName(projectDir)], { shell: false }).catch(() => {});
|
|
3378
|
+
}
|
|
3379
|
+
return { ok: true, id: 'zalo-connect' };
|
|
3380
|
+
}
|
|
3333
3381
|
const installSpec = pluginInstallSpec(id);
|
|
3334
3382
|
const installArgs = ['plugins', 'install', installSpec, '--force'];
|
|
3335
3383
|
if (installSpec.startsWith('clawhub:')) installArgs.push('--acknowledge-clawhub-risk');
|
|
@@ -3632,6 +3680,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3632
3680
|
poster: ['openclaw-n8n-facebook-poster', 'openclaw-facebook-poster', 'facebook-poster'],
|
|
3633
3681
|
fbMessenger: ['openclaw-fb-messenger', 'fb-messenger'],
|
|
3634
3682
|
tencentMemory: ['memory-tencentdb'],
|
|
3683
|
+
zaloConnect: ['zalo-connect', 'openclaw-zalo-connect'],
|
|
3635
3684
|
};
|
|
3636
3685
|
const flags = {
|
|
3637
3686
|
'skill:browser': browserOn,
|
|
@@ -3645,6 +3694,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3645
3694
|
'plugin:openclaw-n8n-facebook-poster': isEnabled(aliases.poster),
|
|
3646
3695
|
'plugin:openclaw-fb-messenger': isEnabled(aliases.fbMessenger),
|
|
3647
3696
|
'plugin:memory-tencentdb': isEnabled(aliases.tencentMemory),
|
|
3697
|
+
'plugin:zalo-connect': isEnabled(aliases.zaloConnect),
|
|
3648
3698
|
};
|
|
3649
3699
|
const extensionsDir = join(projectDir || '', '.openclaw', 'extensions');
|
|
3650
3700
|
const extensionDirExists = (aliases = []) =>
|
|
@@ -3663,6 +3713,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3663
3713
|
'plugin:openclaw-fb-messenger': extensionDirExists(aliases.fbMessenger)
|
|
3664
3714
|
|| aliases.fbMessenger.some((a) => installedKeys.has(a) || Array.from(installedSpecs).some((spec) => spec.includes(a))),
|
|
3665
3715
|
'plugin:memory-tencentdb': isActuallyInstalled(aliases.tencentMemory),
|
|
3716
|
+
'plugin:zalo-connect': isActuallyInstalled(aliases.zaloConnect),
|
|
3666
3717
|
};
|
|
3667
3718
|
const versions = {
|
|
3668
3719
|
'skill:image-gen': await getInstalledSkillVersion(projectDir, aid, 'infographic-generator', cfg),
|
|
@@ -3673,6 +3724,7 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3673
3724
|
'plugin:openclaw-n8n-facebook-poster': await getInstalledPluginVersion(projectDir, aliases.poster),
|
|
3674
3725
|
'plugin:openclaw-fb-messenger': await getInstalledPluginVersion(projectDir, aliases.fbMessenger),
|
|
3675
3726
|
'plugin:memory-tencentdb': await getInstalledPluginVersion(projectDir, aliases.tencentMemory),
|
|
3727
|
+
'plugin:zalo-connect': await getInstalledPluginVersion(projectDir, aliases.zaloConnect),
|
|
3676
3728
|
};
|
|
3677
3729
|
// Docker: fill any plugin version still empty from the container's extensions volume
|
|
3678
3730
|
// (e.g. bundled/clawhub plugins like zalo-mod that aren't on the host). One docker exec.
|
|
@@ -3685,9 +3737,14 @@ async function getFeatureFlags(projectDir, agentId = '') {
|
|
|
3685
3737
|
fillVer('plugin:openclaw-n8n-facebook-poster', aliases.poster);
|
|
3686
3738
|
fillVer('plugin:openclaw-fb-messenger', aliases.fbMessenger);
|
|
3687
3739
|
fillVer('plugin:memory-tencentdb', aliases.tencentMemory);
|
|
3740
|
+
fillVer('plugin:zalo-connect', aliases.zaloConnect);
|
|
3688
3741
|
}
|
|
3689
3742
|
const zaloBackend = fresh.channels?.['zalo-connect']?.enabled ? 'zalo-connect' : '';
|
|
3690
|
-
|
|
3743
|
+
// zalo-connect is REQUIRED by any Zalo bot → lock its toggle (checked, can't disable)
|
|
3744
|
+
// whenever a Zalo binding exists.
|
|
3745
|
+
const hasZaloBot = (fresh.bindings || []).some((b) => b?.match?.channel === 'zalo-connect');
|
|
3746
|
+
const locked = { 'plugin:zalo-connect': hasZaloBot };
|
|
3747
|
+
return { flags, installed, versions, zaloBackend, locked };
|
|
3691
3748
|
}
|
|
3692
3749
|
|
|
3693
3750
|
async function serveStatic(req, res) {
|
|
@@ -3781,6 +3838,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3781
3838
|
const body = await readJson(req);
|
|
3782
3839
|
const osChoice = body.os || detectOs();
|
|
3783
3840
|
const mode = body.mode || recommendedMode(osChoice);
|
|
3841
|
+
const userTimezone = String(body.userTimezone || '').trim() || 'Asia/Ho_Chi_Minh';
|
|
3784
3842
|
const projectDir = body.projectDir ? resolve(String(body.projectDir)) : resolve(rootProjectDir, body.projectName || DEFAULT_PROJECT_NAME);
|
|
3785
3843
|
|
|
3786
3844
|
// Auto-allocate unique, free ports to avoid collision (reserving gatewayPort + 1 for Zalo-mod UI)
|
|
@@ -3810,7 +3868,7 @@ async function handler(req, res, rootProjectDir) {
|
|
|
3810
3868
|
state.gatewayUrl = `http://127.0.0.1:${gatewayPort}`;
|
|
3811
3869
|
state.routerUrl = `http://127.0.0.1:${routerPort}`;
|
|
3812
3870
|
|
|
3813
|
-
installCore({ osChoice, mode, projectDir, gatewayPort, routerPort }).catch(() => {});
|
|
3871
|
+
installCore({ osChoice, mode, projectDir, gatewayPort, routerPort, userTimezone }).catch(() => {});
|
|
3814
3872
|
state.projectDir = projectDir;
|
|
3815
3873
|
state.mode = mode;
|
|
3816
3874
|
state.os = osChoice;
|
|
@@ -4193,18 +4251,39 @@ function sshUserName() { try { return os.userInfo().username || 'root'; } catch
|
|
|
4193
4251
|
function isHeadlessServer() {
|
|
4194
4252
|
return process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
4195
4253
|
}
|
|
4254
|
+
// Kiểm tra nhanh 1 port có đang listen trên host không (tránh forward port chết).
|
|
4255
|
+
function isLocalPortListening(port, host = '127.0.0.1', timeout = 400) {
|
|
4256
|
+
return new Promise((resolve) => {
|
|
4257
|
+
const sock = net.connect({ port, host });
|
|
4258
|
+
let settled = false;
|
|
4259
|
+
const done = (v) => { if (!settled) { settled = true; try { sock.destroy(); } catch {} resolve(v); } };
|
|
4260
|
+
sock.setTimeout(timeout);
|
|
4261
|
+
sock.once('connect', () => done(true));
|
|
4262
|
+
sock.once('timeout', () => done(false));
|
|
4263
|
+
sock.once('error', () => done(false));
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
4196
4266
|
// On a headless server there's no local browser — print an SSH-tunnel command so the
|
|
4197
4267
|
// operator can reach the dashboard AND the Open-web UIs from their own machine. This is
|
|
4198
4268
|
// the discoverable answer for ANY user on a VPS (no manual ssh-config knowledge needed).
|
|
4269
|
+
// CHỈ forward những port đang THỰC SỰ listen trên host — gateway (18789) / 9Router (20128)
|
|
4270
|
+
// thường nằm trong Docker, không bind ra host, nên nếu forward cứng sẽ đẻ ra hàng loạt
|
|
4271
|
+
// "channel: open failed: connect failed: Connection refused" vô nghĩa ở phía client.
|
|
4199
4272
|
async function printRemoteAccessHint(uiPort) {
|
|
4200
4273
|
if (!isHeadlessServer()) return;
|
|
4201
4274
|
const ip = (await getPublicIp()) || '<your-server-ip>';
|
|
4202
|
-
|
|
4275
|
+
// uiPort vừa bind xong nên chắc chắn mở; các port phụ chỉ thêm nếu đang listen.
|
|
4276
|
+
const extras = [];
|
|
4277
|
+
for (const p of [18789, 20128, 18790]) {
|
|
4278
|
+
if (p !== uiPort && await isLocalPortListening(p)) extras.push(p);
|
|
4279
|
+
}
|
|
4280
|
+
const ports = [uiPort, ...extras];
|
|
4281
|
+
const fwd = ports.map((p) => `-L ${p}:127.0.0.1:${p}`).join(' ');
|
|
4203
4282
|
console.log('');
|
|
4204
4283
|
console.log('🌐 No local browser detected (server/VPS). Open the UI from YOUR computer:');
|
|
4205
4284
|
console.log(` ssh ${fwd} ${sshUserName()}@${ip}`);
|
|
4206
4285
|
console.log(` then open: http://localhost:${uiPort}`);
|
|
4207
|
-
console.log(
|
|
4286
|
+
console.log(` (forwards ${ports.length} live port${ports.length > 1 ? 's' : ''}: ${ports.join(', ')})`);
|
|
4208
4287
|
console.log('');
|
|
4209
4288
|
}
|
|
4210
4289
|
|
|
@@ -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.
|
|
@@ -233,6 +238,8 @@
|
|
|
233
238
|
|
|
234
239
|
// ── skills ───────────────────────────────────────────────────────────────
|
|
235
240
|
const skillEntries = buildSkillsEntries(skills, selectedSkills);
|
|
241
|
+
// NOTE: the "zalo-actions" skill was removed — its guidance now lives in the `zalo-connect`
|
|
242
|
+
// tool description itself (fork ≥ v3.0.2), so no per-bot skill entry is needed anymore.
|
|
236
243
|
if (Object.keys(skillEntries).length > 0) {
|
|
237
244
|
cfg.skills = { entries: skillEntries };
|
|
238
245
|
}
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
// Pin the maintained fork tag so setup never falls back to upstream/ClawHub.
|
|
45
45
|
const ZALO_CHANNEL_ID = 'zalo-connect';
|
|
46
46
|
const ZALO_PLUGIN_ID = 'zalo-connect';
|
|
47
|
-
const ZALO_CONNECT_VERSION = '3.0.
|
|
48
|
-
const ZALO_CONNECT_PLUGIN_SPEC = 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.
|
|
47
|
+
const ZALO_CONNECT_VERSION = '3.0.2';
|
|
48
|
+
const ZALO_CONNECT_PLUGIN_SPEC = 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.2';
|
|
49
49
|
|
|
50
50
|
function buildRelayPluginInstallCommand(prefix = 'openclaw') {
|
|
51
51
|
return `if [ ! -d "$OPENCLAW_STATE_DIR/extensions/${TELEGRAM_RELAY_PLUGIN_ID}" ]; then ${prefix} plugins install ${TELEGRAM_RELAY_PLUGIN_SPEC} 2>/dev/null || true; fi`;
|
|
@@ -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,40 @@ 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
|
-
|
|
569
|
-
-
|
|
570
|
-
|
|
571
|
-
- \`message
|
|
572
|
-
-
|
|
573
|
-
|
|
574
|
-
- \`
|
|
575
|
-
- \`
|
|
576
|
-
|
|
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.
|
|
570
|
+
- **\`payload\`**: \`{ "kind": "agentTurn", "message": "<chỉ dẫn/nội dung>" }\`.
|
|
571
|
+
- \`message\` là chỉ dẫn để agent **SINH RA nội dung sẽ gửi** — có thể là câu cố định (\`"Chúc cả nhà ngủ ngon 🌙"\`) hoặc yêu cầu tạo mới mỗi lần (\`"Viết một lời chúc buổi sáng mới, tích cực, không lặp lại"\`). **Phần trả lời của agent CHÍNH LÀ tin được gửi.**
|
|
572
|
+
- ⛔ **KHÔNG** viết kiểu mệnh lệnh gửi ("dùng tool ... gửi tới group ...", "gửi tin tới groupId ..."). ⛔ **KHÔNG** để job tự gọi tool gửi tin (message/send/zalo-connect). Việc gửi là do \`delivery.announce\` lo — tự gọi sẽ lỗi \`Unknown target\` / "Cron failed".
|
|
573
|
+
- **\`delivery\`** — 🚨 **BẮT BUỘC** cho tin gửi tới người/nhóm. Isolated job reset routing mỗi lần chạy, nên phải ghi rõ \`channel\` + \`to\`; thiếu → lỗi \`Refusing implicit isolated cron delivery\` (không gửi được):
|
|
574
|
+
- \`mode\`: \`"announce"\`. ⛔ **KHÔNG dùng \`"none"\`** — job vẫn chạy "ok" nhưng KHÔNG gửi gì (đây là bẫy thường gặp).
|
|
575
|
+
- \`channel\`: \`"${zaloDeliveryChannel}"\` (KHÔNG để \`"last"\`).
|
|
576
|
+
- \`to\`: **ID THÔ, KHÔNG tiền tố**. Group Zalo → groupId thẳng (vd \`"1925989252066183028"\`); DM → userId. ⚠️ **KHÔNG thêm \`g:\`** (sẽ lỗi \`Unknown target "g:..."\`).
|
|
577
|
+
|
|
578
|
+
### 📦 Ví dụ A — LẶP LẠI (chúc buổi sáng 9:05 mỗi ngày cho 1 nhóm):
|
|
579
|
+
\`\`\`json
|
|
580
|
+
{
|
|
581
|
+
"sessionTarget": "isolated",
|
|
582
|
+
"wakeMode": "now",
|
|
583
|
+
"schedule": { "kind": "cron", "expr": "5 9 * * *", "tz": "${tz}" },
|
|
584
|
+
"payload": { "kind": "agentTurn", "message": "Viết MỘT lời chúc buổi sáng MỚI mỗi ngày, tích cực, ấm áp, lịch sự, không lặp lại. Chỉ trả về đúng nội dung lời chúc." },
|
|
585
|
+
"delivery": { "mode": "announce", "channel": "${zaloDeliveryChannel}", "to": "1228185345777623323" }
|
|
586
|
+
}
|
|
587
|
+
\`\`\`
|
|
588
|
+
|
|
589
|
+
### 📦 Ví dụ B — MỘT LẦN (giờ địa phương) — mỗi group 1 job riêng:
|
|
590
|
+
\`\`\`json
|
|
591
|
+
{
|
|
592
|
+
"sessionTarget": "isolated",
|
|
593
|
+
"wakeMode": "now",
|
|
594
|
+
"schedule": { "kind": "at", "at": "2026-07-22T00:30:00", "tz": "${tz}" },
|
|
595
|
+
"payload": { "kind": "agentTurn", "message": "Chúc cả nhà ngủ ngon 🌙" },
|
|
596
|
+
"delivery": { "mode": "announce", "channel": "${zaloDeliveryChannel}", "to": "1925989252066183028" }
|
|
597
|
+
}
|
|
598
|
+
\`\`\`
|
|
577
599
|
|
|
578
600
|
## 2. Tìm kiếm, Tắt, Bật, Xóa Lịch
|
|
579
601
|
- **Xem danh sách:** Gọi \`cron\` với action \`list\` (kèm \`includeDisabled: true\`). Tìm \`id\` phù hợp.
|
|
@@ -586,6 +608,54 @@ Truyền tham số \`job\` (object) gồm:
|
|
|
586
608
|
\`openclaw cron run <job_id> --wait\` (hoặc gọi tool \`cron\` với action \`run\` kèm \`id\`).`;
|
|
587
609
|
}
|
|
588
610
|
|
|
611
|
+
// DEPRECATED (unused): guidance moved into the `zalo-connect` tool description (fork ≥ v3.0.2).
|
|
612
|
+
// Kept only for reference; no longer wired into buildWorkspaceFileMap.
|
|
613
|
+
function buildZaloActionsSkillMd(isVi = true, zaloDeliveryChannel = 'zalo-connect') {
|
|
614
|
+
return `---
|
|
615
|
+
name: zalo-actions
|
|
616
|
+
description: Hành động Zalo phong phú qua tool ${zaloDeliveryChannel} — sticker, thả cảm xúc, bình chọn, ghi chú, nhắc hẹn, media, quản trị nhóm.
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
# 💬 Hành động Zalo (tool: \`${zaloDeliveryChannel}\`)
|
|
620
|
+
|
|
621
|
+
Ngoài gửi text, tool \`${zaloDeliveryChannel}\` có ~149 action. Gọi bằng: tool \`${zaloDeliveryChannel}\` với \`{ "action": "<tên>", ... }\`. Cứ chủ động dùng khi phù hợp (user xin sticker, cần bình chọn, nhắc hẹn, ghim ghi chú...).
|
|
622
|
+
|
|
623
|
+
## Quy tắc chung
|
|
624
|
+
- **\`threadId\`** = nơi gửi. Trong GROUP hiện tại → dùng **groupId THÔ** (KHÔNG tiền tố \`g:\`) kèm **\`isGroup: true\`**. DM cá nhân → \`threadId\` = userId, \`isGroup: false\`.
|
|
625
|
+
- Làm xong thì trả lời ngắn gọn cho user, KHÔNG dán JSON/kết quả thô.
|
|
626
|
+
|
|
627
|
+
## 🎨 Sticker — \`send-sticker\`
|
|
628
|
+
- Dễ nhất (theo từ khoá, tool tự tìm & gửi): \`{ "action": "send-sticker", "threadId": "<groupId>", "isGroup": true, "keyword": "chào buổi sáng" }\`.
|
|
629
|
+
- Chỉ định cụ thể: thêm \`"stickerId"\` + \`"stickerCateId"\` (thay cho \`keyword\`).
|
|
630
|
+
|
|
631
|
+
## 😀 Thả cảm xúc — \`add-reaction\`
|
|
632
|
+
- \`{ "action": "add-reaction", "msgId": "<id tin nhắn>", "icon": "heart" }\` — \`icon\`: heart / like / haha / wow / cry / angry.
|
|
633
|
+
|
|
634
|
+
## 📊 Bình chọn — \`create-poll\`
|
|
635
|
+
- \`{ "action": "create-poll", "threadId": "<groupId>", "isGroup": true, "title": "Câu hỏi?", "options": ["A","B"], "allowMultiChoices": false }\`.
|
|
636
|
+
|
|
637
|
+
## 📌 Ghi chú ghim — \`create-note\`
|
|
638
|
+
- \`{ "action": "create-note", "threadId": "<groupId>", "isGroup": true, "title": "Nội dung ghi chú" }\`.
|
|
639
|
+
|
|
640
|
+
## ⏰ Nhắc hẹn nhóm — \`create-reminder\`
|
|
641
|
+
- \`{ "action": "create-reminder", "threadId": "<groupId>", "isGroup": true, "title": "...", "startTime": <epoch ms>, "repeat": 0 }\` (repeat: 0 không lặp / 1 ngày / 2 tuần / 3 tháng). Lịch định kỳ kiểu cron → dùng skill \`cronjob\`.
|
|
642
|
+
|
|
643
|
+
## 🖼️ Media
|
|
644
|
+
- Ảnh: \`send-image\` \`{threadId,isGroup,url}\` · Video: \`send-video\` \`{threadId,isGroup,url,thumbnailUrl?}\` · Voice: \`send-voice\` \`{threadId,isGroup,voiceUrl}\` · File: \`send-file\` \`{threadId,isGroup,filePath}\` · Link preview: \`send-link\` \`{threadId,isGroup,url}\`.
|
|
645
|
+
|
|
646
|
+
## ↪️ Chuyển tiếp / thu hồi / xoá
|
|
647
|
+
- \`forward-message\` \`{msgId, threadIds:["<đích>"]}\` · \`delete-message\` \`{msgId, threadId, onlyMe?}\`.
|
|
648
|
+
- **Thu hồi tin của CHÍNH bot** (\`undo-message\`): khi user bảo "thu hồi/gỡ tin vừa nãy", gọi \`undo-message\` \`{threadId:"<groupId/userId hiện tại>"}\` — **KHÔNG cần msgId**, hệ thống tự lấy tin gần nhất bot đã gửi trong luồng đó. ⏳ Chỉ thu hồi được tin bot gửi **trong 5 phút** gần nhất; tin cũ hơn (hoặc tin của người khác) thì báo lại là không gỡ được. Muốn nhắm tin cụ thể thì truyền thêm \`{msgId}\`.
|
|
649
|
+
|
|
650
|
+
## 🛠️ Quản trị nhóm (bot phải là admin)
|
|
651
|
+
- \`add-group-admin\` / \`remove-group-admin\` \`{groupId,userId}\` · \`rename-group\` \`{groupId,groupName}\` · \`change-group-owner\` \`{groupId,userId}\` · \`invite-to-groups\` \`{userId, groupIds:[...]}\` · \`update-group-settings\` \`{groupId, groupSettings:{...}}\` · link nhóm: \`enable-group-link\`/\`disable-group-link\`/\`get-group-link\` \`{groupId}\`.
|
|
652
|
+
|
|
653
|
+
## 📎 Hội thoại
|
|
654
|
+
- Ghim: \`pin-conversation\` \`{threadId}\` · Tắt báo: \`mute-conversation\` \`{threadId, duration:-1}\` (giây, -1 = mãi) · Đang gõ "…": \`send-typing\` \`{threadId,isGroup}\`.
|
|
655
|
+
|
|
656
|
+
> Còn nhiều action khác (tìm bạn, QR, catalog/sản phẩm, auto-reply, quick message, poll nâng cao, mã hoá tin tự huỷ...). Xem mô tả từng \`action\` + tham số ngay trong schema của tool \`${zaloDeliveryChannel}\`.`;
|
|
657
|
+
}
|
|
658
|
+
|
|
589
659
|
function buildInfographicGeneratorSkillMd(botName = 'Williams') {
|
|
590
660
|
return '';
|
|
591
661
|
}
|
|
@@ -1304,6 +1374,7 @@ Add whatever helps you do your job. This is your cheat sheet.
|
|
|
1304
1374
|
hasImageGen = false,
|
|
1305
1375
|
hasZaloMod = false,
|
|
1306
1376
|
zaloBackend = 'zalo-connect',
|
|
1377
|
+
userTimezone = 'Asia/Ho_Chi_Minh',
|
|
1307
1378
|
} = opts;
|
|
1308
1379
|
|
|
1309
1380
|
const isMultiBot = variant === 'relay';
|
|
@@ -1330,9 +1401,13 @@ Add whatever helps you do your job. This is your cheat sheet.
|
|
|
1330
1401
|
}
|
|
1331
1402
|
|
|
1332
1403
|
if (hasScheduler) {
|
|
1333
|
-
files['skills/cronjob/SKILL.md'] = buildCronjobSkillMd(isVi, 'zalo-connect');
|
|
1404
|
+
files['skills/cronjob/SKILL.md'] = buildCronjobSkillMd(isVi, 'zalo-connect', userTimezone);
|
|
1334
1405
|
}
|
|
1335
1406
|
|
|
1407
|
+
// NOTE: the old `zalo-actions` agent skill was removed — its guidance (Zalo conventions +
|
|
1408
|
+
// action recipes) now lives directly in the `zalo-connect` tool description (fork ≥ v3.0.2),
|
|
1409
|
+
// so every zalo-connect bot gets it automatically without a generated per-bot skill.
|
|
1410
|
+
|
|
1336
1411
|
if (hasImageGen) {
|
|
1337
1412
|
files['skills/infographic-generator/SKILL.md'] = buildInfographicGeneratorSkillMd(botName);
|
|
1338
1413
|
files['skills/infographic-generator/image-generator.js'] = buildInfographicGeneratorJs();
|
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: {}, featureLocked: {}, 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 || {};
|
|
@@ -1010,6 +1064,7 @@ function botSkillsPanel() {
|
|
|
1010
1064
|
{ id: 'learning-memory', title: 'Siêu Trí Nhớ Dài Hạn (learning-memory)', desc: 'Tự động ghi nhớ bài học vào MEMORY.md, tự đóng gói và tiến hóa kỹ năng mới vào skills/' },
|
|
1011
1065
|
];
|
|
1012
1066
|
const plugins = [
|
|
1067
|
+
{ 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'] },
|
|
1013
1068
|
{ id: 'memory-tencentdb', title: 'TencentDB Agent Memory', desc: 'Bộ nhớ phân tầng L0–L3 + nén ngữ cảnh: nhớ tốt session dài, tiết kiệm ~61% token (local SQLite, không cần API)' },
|
|
1014
1069
|
{ id: 'openclaw-browser-automation', title: 'openclaw-browser-automation', desc: 'Smart Search + Browser (headless & Chrome thật)' },
|
|
1015
1070
|
{ id: 'openclaw-zalo-mod', title: 'openclaw-zalo-mod', desc: 'Zalo group helpers', channels: ['zalo-personal'], openWebPort: 18790, openWebPath: '/dashboard' },
|
|
@@ -1029,8 +1084,9 @@ function botSkillsPanel() {
|
|
|
1029
1084
|
const row = (item, group) => {
|
|
1030
1085
|
const key = `${group}:${item.id}`;
|
|
1031
1086
|
const loading = !!state.featureLoading[key];
|
|
1087
|
+
const locked = !!state.featureLocked?.[key];
|
|
1032
1088
|
const requiresInstall = group === 'plugin' || (group === 'skill' && (item.id === 'image-gen' || item.id === 'learning-memory'));
|
|
1033
|
-
const isInstalled = !requiresInstall || !!state.featureInstalled?.[key];
|
|
1089
|
+
const isInstalled = !requiresInstall || !!state.featureInstalled?.[key] || locked;
|
|
1034
1090
|
|
|
1035
1091
|
const version = requiresInstall && isInstalled ? (state.featureVersions?.[key] || '') : '';
|
|
1036
1092
|
const versionBadge = version ? `<span class="plugin-version-badge" style="display:inline-block; font-size: 11px; background: rgba(66, 133, 244, 0.15); color: #4285F4; padding: 2px 6px; border-radius: 4px; font-weight: 600; border: 1px solid rgba(66,133,244,0.25); flex:0 0 auto;">v${escapeHtml(version)}</span>` : '';
|
|
@@ -1040,7 +1096,7 @@ function botSkillsPanel() {
|
|
|
1040
1096
|
let toggleHtml = '';
|
|
1041
1097
|
const secs = [];
|
|
1042
1098
|
if (isInstalled) {
|
|
1043
|
-
toggleHtml = `<label class="feature-switch"><input type="checkbox" data-feature-toggle="${key}" ${flags[key] ? 'checked' : ''} ${loading ? 'disabled' : ''}/><span></span></label
|
|
1099
|
+
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') : ''}"><input type="checkbox" data-feature-toggle="${key}" ${(flags[key] || locked) ? 'checked' : ''} ${(loading || locked) ? 'disabled' : ''}/><span></span></label>${locked ? `<span title="${t('Đã khoá','Locked')}" style="margin-left:6px;opacity:.55;font-size:12px;">🔒</span>` : ''}`;
|
|
1044
1100
|
if (item.openWebPort) {
|
|
1045
1101
|
secs.push(`<a class="secondary icon-btn2" href="${sameHostUrl('', item.openWebPort)}${item.openWebPath || ''}" target="_blank" rel="noopener" title="${t('Mở dashboard của plugin','Open the plugin dashboard')}" style="padding: 4px 8px; font-size: 11px; height: 28px; border-width: 1px;">${actionIcon('link')}<span>${t('Mở web','Open')}</span></a>`);
|
|
1046
1102
|
}
|
|
@@ -1126,8 +1182,16 @@ function wireTab() {
|
|
|
1126
1182
|
localStorage.setItem('openclaw-'+btn.dataset.pref, btn.dataset.value);
|
|
1127
1183
|
const main = $('#app-main-content');
|
|
1128
1184
|
if (main) main.remove();
|
|
1129
|
-
render();
|
|
1185
|
+
render();
|
|
1186
|
+
});
|
|
1187
|
+
document.querySelectorAll('[data-pref-switch]').forEach(sw => sw.onchange = () => {
|
|
1188
|
+
const pref = sw.dataset.prefSwitch;
|
|
1189
|
+
const val = pref === 'theme' ? (sw.checked ? 'dark' : 'light') : (sw.checked ? 'on' : 'off');
|
|
1190
|
+
state[pref] = val;
|
|
1191
|
+
localStorage.setItem('openclaw-' + pref, val);
|
|
1192
|
+
applyPrefs();
|
|
1130
1193
|
});
|
|
1194
|
+
{ const tzSel = $('#tz-select'); if (tzSel) tzSel.onchange = () => { state.tz = tzSel.value; localStorage.setItem('openclaw-tz', tzSel.value); }; }
|
|
1131
1195
|
document.querySelectorAll('[data-tab-jump]').forEach(btn => btn.onclick = () => { state.tab = btn.dataset.tabJump; render(); });
|
|
1132
1196
|
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
1197
|
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 +1457,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1393
1457
|
os: form.querySelector('[name="os"]')?.value || state.installDraft?.os || state.os,
|
|
1394
1458
|
mode: form.querySelector('[name="mode"]')?.value || state.installTab || state.installDraft?.mode || state.mode,
|
|
1395
1459
|
projectDir: form.querySelector('[name=\"projectDir\"]')?.value || '',
|
|
1460
|
+
userTimezone: state.tz,
|
|
1396
1461
|
};
|
|
1397
1462
|
if (!body.projectDir) throw new Error(t('Chưa có đường dẫn project','Missing project path'));
|
|
1398
1463
|
await api('/api/install', { method: 'POST', body });
|
|
@@ -1429,6 +1494,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
|
|
|
1429
1494
|
const body = Object.fromEntries(fd.entries());
|
|
1430
1495
|
body.projectDir = activeProjectDir();
|
|
1431
1496
|
body.channel = state.botChannel || 'telegram';
|
|
1497
|
+
body.userTimezone = state.tz;
|
|
1432
1498
|
if (body.channel === 'zalo-personal') {
|
|
1433
1499
|
state.botModalOpen = false;
|
|
1434
1500
|
state.zaloLoginOpen = true;
|
|
@@ -1543,6 +1609,7 @@ async function loadFeatureFlags(silent=false){
|
|
|
1543
1609
|
state.featureFlags = data.flags || {};
|
|
1544
1610
|
state.featureInstalled = data.installed || {};
|
|
1545
1611
|
state.featureVersions = data.versions || {};
|
|
1612
|
+
state.featureLocked = data.locked || {};
|
|
1546
1613
|
state.zaloBackend = data.zaloBackend || '';
|
|
1547
1614
|
if (state.zaloBackend === 'zalo-connect') await loadZaloHealth(true).catch(() => {});
|
|
1548
1615
|
} catch (_) {
|
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)}}
|