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 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.3-0EA5E9?style=for-the-badge" alt="Version 5.13.3" /></a>
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.0
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.3-0EA5E9?style=for-the-badge" alt="Version 5.13.3" /></a>
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.0
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
- const fwd = [uiPort, 18789, 20128, 18790].map((p) => `-L ${p}:127.0.0.1:${p}`).join(' ');
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(' (forwards the dashboard + OpenClaw gateway + 9Router + zalo-mod web UIs)');
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\`: \`"cron"\` (lặp lại) hoặc \`"at"\` (một lần).
567
- - \`expr\`: Biểu thức cron (ví dụ: \`"0 7 * * *"\`).
568
- - \`tz\`: Bắt buộc điền múi giờ, dụ: \`"Asia/Ho_Chi_Minh"\`.
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 ( 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), 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\`: Nội dung tin nhắn nhắc nhở.
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\`: ID người nhận hoặc ID nhóm.
576
- - ⚠️ **QUAN TRỌNG:** Nếu gửi tới Group Zalo, ID nhóm bắt buộc phải thêm tiền tố **\`g:\`** ở đầu (Ví dụ: \`g:1925989252066183028\`). Nếu thiếu \`g:\`, tin nhắn sẽ bị gửi nhầm thành tin cá nhân (DM) hoặc lỗi.
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;
@@ -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: center the 4 items (overrides an earlier repeat(5,1fr) !important rule) */
1917
- .bottom{grid-template-columns:repeat(4,1fr)!important}
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)}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openclaw-bot",
3
- "version": "5.13.3",
3
+ "version": "5.13.4",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {