create-openclaw-bot 5.13.0 → 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.0-0EA5E9?style=for-the-badge" alt="Version 5.13.0" /></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,14 +37,22 @@
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
- <summary><b>Previous: What's new in v5.13.0</b></summary>
55
+ <summary><b>Previous: What's new in v5.12.0</b></summary>
48
56
 
49
57
  - 💬 **OpenClaw Zalo Connect, ready in one click**: create a personal Zalo bot or press **Zalo Login** and Setup prepares the channel automatically, then opens the QR flow in the dashboard.
50
58
  - ⚡ **Install once, reconnect faster**: an existing Zalo Connect installation is reused instead of being downloaded again on every login or restart.
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.0-0EA5E9?style=for-the-badge" alt="Version 5.13.0" /></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,14 +37,22 @@
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
- <summary><b>Trước đó: Có gì mới trong v5.13.0</b></summary>
55
+ <summary><b>Trước đó: Có gì mới trong v5.12.0</b></summary>
48
56
 
49
57
  - 💬 **OpenClaw Zalo Connect, sẵn sàng trong một chạm**: tạo bot Zalo cá nhân hoặc bấm **Đăng nhập Zalo**, Setup tự chuẩn bị channel rồi mở luồng quét QR ngay trên dashboard.
50
58
  - ⚡ **Cài một lần, kết nối lại nhanh hơn**: nếu Zalo Connect đã có, Setup dùng lại bản hiện tại thay vì tải lại mỗi lần đăng nhập hay restart.
@@ -759,6 +759,10 @@ async function detectRuntime(projectDir) {
759
759
  const _runtimeSynced = new Set();
760
760
  async function syncRuntimeState(projectDir, { full = false } = {}) {
761
761
  if (!projectDir || !existsSync(join(projectDir, '.openclaw', 'openclaw.json'))) return;
762
+ // New OpenClaw builds may create this directory during probing even when no
763
+ // attestation exists. Remove only the empty directory so it does not clutter
764
+ // the project tree; never touch a real `.attested` file.
765
+ await removeEmptyWorkspaceAttestations(projectDir).catch(() => {});
762
766
  const firstSync = full || !_runtimeSynced.has(projectDir);
763
767
  if (firstSync) {
764
768
  // Auto-migrate legacy /root/project paths → /home/node/project in openclaw.json
@@ -786,6 +790,15 @@ async function syncRuntimeState(projectDir, { full = false } = {}) {
786
790
  _runtimeSynced.add(projectDir);
787
791
  }
788
792
 
793
+ async function removeEmptyWorkspaceAttestations(projectDir) {
794
+ const attestDir = join(projectDir, '.openclaw', 'workspace-attestations');
795
+ if (!existsSync(attestDir)) return false;
796
+ const entries = await fsp.readdir(attestDir);
797
+ if (entries.length > 0) return false;
798
+ await fsp.rmdir(attestDir);
799
+ return true;
800
+ }
801
+
789
802
  /**
790
803
  * Migrate legacy /root/project/ paths to /home/node/project/ in openclaw.json.
791
804
  * Old projects may have been created with /root/project/ which doesn't match the
@@ -1396,6 +1409,10 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
1396
1409
  const userDesc = String(body.userDescription || body.userDesc || '').trim();
1397
1410
  const userInfo = [userName ? `- **Tên:** ${userName}` : '', userDesc ? `- **Mô tả:** ${userDesc}` : ''].filter(Boolean).join('\n');
1398
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
+
1399
1416
  const openclawHome = join(projectDir, '.openclaw');
1400
1417
  await fsp.mkdir(openclawHome, { recursive: true });
1401
1418
  const cfgPath = join(openclawHome, 'openclaw.json');
@@ -1408,7 +1425,9 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
1408
1425
  selectedSkills: ['memory', 'web-search', 'scheduler'],
1409
1426
  skills: dataExport.SKILLS || [],
1410
1427
  agentMetas: [],
1428
+ userTimezone: bodyTz,
1411
1429
  }));
1430
+ const userTimezone = cfg.agents?.defaults?.userTimezone || bodyTz;
1412
1431
 
1413
1432
  const used = new Set(cfg.agents.list.map((a) => a.id));
1414
1433
  const botName = uniqueDisplayName(requestedBotName, new Set(cfg.agents.list.map((a) => a.name || a.id)));
@@ -1496,6 +1515,7 @@ async function createBotInProject(projectDir, body = {}, runtime = {}) {
1496
1515
  zaloBackend: zaloBackendForConfig(cfg),
1497
1516
  hasScheduler,
1498
1517
  hasImageGen,
1518
+ userTimezone,
1499
1519
  });
1500
1520
  const wsRoot = join(openclawHome, workspaceDir);
1501
1521
  for (const [name, content] of Object.entries(files)) {
@@ -1618,6 +1638,7 @@ async function updateBotInProject(projectDir, agentId, body = {}, runtime = {})
1618
1638
  zaloBackend: zaloBackendForConfig(cfg),
1619
1639
  hasScheduler,
1620
1640
  hasImageGen,
1641
+ userTimezone: cfg.agents?.defaults?.userTimezone || 'Asia/Ho_Chi_Minh',
1621
1642
  });
1622
1643
  const wsRoot = join(projectDir, '.openclaw', workspaceDir);
1623
1644
  for (const [name, content] of Object.entries(files)) {
@@ -2313,7 +2334,7 @@ function json(res, data, status = 200) {
2313
2334
  res.end(body);
2314
2335
  }
2315
2336
 
2316
- 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' }) {
2317
2338
  await fsp.mkdir(projectDir, { recursive: true });
2318
2339
  const openclawHome = join(projectDir, '.openclaw');
2319
2340
  await fsp.mkdir(openclawHome, { recursive: true });
@@ -2321,7 +2342,7 @@ async function writeCoreProject({ projectDir, osChoice, mode, gatewayPort = 1878
2321
2342
 
2322
2343
  const selectedSkills = ['memory', 'web-search', 'scheduler'];
2323
2344
  const agentMetas = [];
2324
- 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 };
2325
2346
  const cfg = buildOpenclawJson(common);
2326
2347
  // A core project has no channel account yet. Keep its environment shared/credential-free;
2327
2348
  // writing the literal <your_bot_token> placeholder makes OpenClaw auto-detect Telegram and
@@ -2549,7 +2570,7 @@ async function ensureDockerInstalled(osChoice) {
2549
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.');
2550
2571
  }
2551
2572
 
2552
- 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' }) {
2553
2574
  state.installing = true;
2554
2575
  state.installed = false;
2555
2576
  state.lastError = null;
@@ -2563,7 +2584,7 @@ async function installCore({ osChoice, mode, projectDir, gatewayPort = 18789, ro
2563
2584
  // Make sure Docker is present (auto-install on Linux/VPS) before doing any work — fail fast
2564
2585
  // with a clear message rather than deep inside `docker compose up`.
2565
2586
  await ensureDockerInstalled(osChoice);
2566
- await writeCoreProject({ projectDir, osChoice, mode, gatewayPort, routerPort });
2587
+ await writeCoreProject({ projectDir, osChoice, mode, gatewayPort, routerPort, userTimezone });
2567
2588
  await run('npm', ['install', '-g', OPENCLAW_NPM_SPEC]);
2568
2589
  await run('npm', ['install', '-g', NINE_ROUTER_NPM_SPEC]);
2569
2590
  if (mode === 'docker') {
@@ -3276,7 +3297,7 @@ async function installFeature(projectDir, agentId, kind, id) {
3276
3297
  const botContainer = getBotContainerName(projectDir);
3277
3298
  sendLog(`[skill] Installing/updating clawhub:${slug} inside container ${botContainer} for agent ${agentId}...`);
3278
3299
 
3279
- const cmd = `cd /home/node/project && openclaw skills install ${slug} --agent ${agentId} --force`;
3300
+ const cmd = `cd /home/node/project && openclaw skills install ${slug} --agent ${agentId} --force --acknowledge-clawhub-risk`;
3280
3301
  const cmdOut = await runCapture('docker', ['exec', botContainer, 'sh', '-lc', cmd], { cwd: projectDir, shell: false });
3281
3302
 
3282
3303
  if (cmdOut) {
@@ -3298,7 +3319,7 @@ async function installFeature(projectDir, agentId, kind, id) {
3298
3319
  await run('openclaw', ['doctor', '--fix'], { cwd: projectDir, env: openclawProjectEnv(projectDir) }).catch((err) => sendLog(`[skill] doctor --fix skipped: ${err.message}`));
3299
3320
  sendLog(`[skill] Installing clawhub:${slug} for agent ${agentId}...`);
3300
3321
 
3301
- await run('openclaw', ['skills', 'install', slug, '--agent', agentId, '--force'], {
3322
+ await run('openclaw', ['skills', 'install', slug, '--agent', agentId, '--force', '--acknowledge-clawhub-risk'], {
3302
3323
  cwd: projectDir,
3303
3324
  env: openclawProjectEnv(projectDir)
3304
3325
  });
@@ -3768,6 +3789,7 @@ async function handler(req, res, rootProjectDir) {
3768
3789
  const body = await readJson(req);
3769
3790
  const osChoice = body.os || detectOs();
3770
3791
  const mode = body.mode || recommendedMode(osChoice);
3792
+ const userTimezone = String(body.userTimezone || '').trim() || 'Asia/Ho_Chi_Minh';
3771
3793
  const projectDir = body.projectDir ? resolve(String(body.projectDir)) : resolve(rootProjectDir, body.projectName || DEFAULT_PROJECT_NAME);
3772
3794
 
3773
3795
  // Auto-allocate unique, free ports to avoid collision (reserving gatewayPort + 1 for Zalo-mod UI)
@@ -3797,7 +3819,7 @@ async function handler(req, res, rootProjectDir) {
3797
3819
  state.gatewayUrl = `http://127.0.0.1:${gatewayPort}`;
3798
3820
  state.routerUrl = `http://127.0.0.1:${routerPort}`;
3799
3821
 
3800
- installCore({ osChoice, mode, projectDir, gatewayPort, routerPort }).catch(() => {});
3822
+ installCore({ osChoice, mode, projectDir, gatewayPort, routerPort, userTimezone }).catch(() => {});
3801
3823
  state.projectDir = projectDir;
3802
3824
  state.mode = mode;
3803
3825
  state.os = osChoice;
@@ -4180,18 +4202,39 @@ function sshUserName() { try { return os.userInfo().username || 'root'; } catch
4180
4202
  function isHeadlessServer() {
4181
4203
  return process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
4182
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
+ }
4183
4217
  // On a headless server there's no local browser — print an SSH-tunnel command so the
4184
4218
  // operator can reach the dashboard AND the Open-web UIs from their own machine. This is
4185
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.
4186
4223
  async function printRemoteAccessHint(uiPort) {
4187
4224
  if (!isHeadlessServer()) return;
4188
4225
  const ip = (await getPublicIp()) || '<your-server-ip>';
4189
- 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(' ');
4190
4233
  console.log('');
4191
4234
  console.log('🌐 No local browser detected (server/VPS). Open the UI from YOUR computer:');
4192
4235
  console.log(` ssh ${fwd} ${sshUserName()}@${ip}`);
4193
4236
  console.log(` then open: http://localhost:${uiPort}`);
4194
- 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(', ')})`);
4195
4238
  console.log('');
4196
4239
  }
4197
4240
 
@@ -4210,4 +4253,4 @@ export async function startLocalInstaller({ host = '127.0.0.1', preferredPort =
4210
4253
  printRemoteAccessHint(port).catch(() => {});
4211
4254
  }
4212
4255
 
4213
- export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, buildZaloHealthSnapshot };
4256
+ export { createBotInProject, updateBotInProject, deleteBotInProject, validateOpenclawConfig, startZaloLogin, readBotCredentials, resolveProject9RouterApiKey, installCore, deleteProjectFolder, buildZaloHealthSnapshot, removeEmptyWorkspaceAttestations };
@@ -62,10 +62,10 @@
62
62
  credSteps: [
63
63
  { textVi: '⚠️ Zalo cá nhân chạy qua <strong>OpenClaw Zalo Connect (unofficial, zca-js)</strong> — tự động hoá tài khoản cá nhân có thể vi phạm điều khoản Zalo và khiến tài khoản bị hạn chế. <strong>Chỉ nên dùng tài khoản phụ.</strong>', textEn: '⚠️ Personal Zalo runs on <strong>OpenClaw Zalo Connect (unofficial, zca-js)</strong> — automating a personal account may violate Zalo terms and can get the account restricted. <strong>Use a secondary account.</strong>' },
64
64
  { textVi: 'Sau khi tạo bot, bấm <strong>Đăng nhập Zalo</strong> để quét QR ngay trong giao diện. Nếu cần chạy thủ công: <code>openclaw channels login --channel zalo-connect --account default</code>.', textEn: 'After creating the bot, click <strong>Zalo Login</strong> to scan the QR right in the UI. Manual fallback: <code>openclaw channels login --channel zalo-connect --account default</code>.' },
65
- { textVi: 'Mặc định an toàn: DM theo chế pairing, group tắt hết sau khi quét QR hãy tự chọn DM/nhóm được phép trong cấu hình.', textEn: 'Secure defaults: DM uses pairing, all groups start disabled after the QR scan, explicitly choose the allowed DMs/groups in the config.' },
65
+ { textVi: 'Sau khi quét QR, bot nhận được tin nhắn xác nhận Owner ngay; bạn thể siết lại DM/nhóm sau trong cấu hình.', textEn: 'After QR login, the bot can receive the initial Owner confirmation immediately; you can tighten DM/group access later in config.' },
66
66
  ],
67
67
  // Mirrors buildZaloConnectChannelConfig() in bot-config-gen.js — keys validated
68
- // against OpenClaw Zalo Connect 3.0.0's strict channel schema.
68
+ // against OpenClaw Zalo Connect 3.0.1's strict channel schema.
69
69
  channelConfig: {
70
70
  'zalo-connect': {
71
71
  enabled: true,
@@ -73,11 +73,11 @@
73
73
  accounts: {
74
74
  default: { enabled: true },
75
75
  },
76
- dmPolicy: 'pairing',
77
- allowFrom: [],
76
+ dmPolicy: 'open',
77
+ allowFrom: ['*'],
78
78
  groupPolicy: 'allowlist',
79
79
  groups: {
80
- '*': { enabled: false, requireMention: true },
80
+ '*': { enabled: true, requireMention: false },
81
81
  },
82
82
  },
83
83
  },
@@ -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.
@@ -310,10 +315,9 @@
310
315
  channels.telegram = telegramConfig;
311
316
  } else if (isZaloPersonal(channelKey)) {
312
317
  // Zalo Personal → OpenClaw Zalo Connect, the only personal-Zalo backend.
313
- // Secure defaults: DM pairing, empty
314
- // allowFrom, all groups disabled + requireMention until the owner explicitly
315
- // enables them post-QR. Never default to `allowFrom: ["*"]` / open groups.
316
- // Every key below is validated against OpenClaw Zalo Connect 3.0.0's schema
318
+ // First-run defaults keep owner confirmation reachable immediately after QR:
319
+ // DMs are open to every sender and groups are enabled without requiring a mention.
320
+ // Every key below is validated against OpenClaw Zalo Connect 3.0.1's schema
317
321
  // (`additionalProperties: false`) — do NOT add keys (e.g. historyLimit,
318
322
  // groupAllowFrom) without re-checking `openclaw.plugin.json` first, an unknown
319
323
  // key crashes the gateway on boot.
@@ -336,11 +340,11 @@
336
340
  accounts: {
337
341
  default: { enabled: true },
338
342
  },
339
- dmPolicy: 'pairing',
340
- allowFrom: [],
343
+ dmPolicy: 'open',
344
+ allowFrom: ['*'],
341
345
  groupPolicy: 'allowlist',
342
346
  groups: {
343
- '*': { enabled: false, requireMention: true },
347
+ '*': { enabled: true, requireMention: false },
344
348
  },
345
349
  };
346
350
  }
@@ -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.0';
48
- const ZALO_CONNECT_PLUGIN_SPEC = 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.0';
47
+ const ZALO_CONNECT_VERSION = '3.0.1';
48
+ const ZALO_CONNECT_PLUGIN_SPEC = 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.1';
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`;
@@ -244,10 +244,10 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
244
244
  const stateVolDecl = useStateVolume ? '\n openclaw-state:' : '';
245
245
  const stateVolBlock = useStateVolume ? '\n\nvolumes:\n openclaw-state:' : '';
246
246
  const skillLines = dockerfileSkillInstallMode === 'build' && allSkills.length > 0
247
- ? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
247
+ ? `\n# Install skills (ClawHub)\n${allSkills.map((skill) => `RUN openclaw skills install ${skill} --acknowledge-clawhub-risk || echo "Warning: Failed to install ${skill} due to rate limits."`).join('\n')}\n`
248
248
  : '';
249
249
  const pluginLines = dockerfilePlugins.length > 0
250
- ? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
250
+ ? `\n# Install plugins (ClawHub)\n${dockerfilePlugins.map((p) => `RUN openclaw plugins install ${p} --acknowledge-clawhub-risk || echo "Warning: Failed to install plugin ${p}"`).join('\n')}\n`
251
251
  : '';
252
252
  const patchLine = `RUN node -e "const fs=require('fs');const path=require('path');const dir='/usr/local/lib/node_modules/openclaw/dist';const from='\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const to='\\t\\t\\t\\t\\ttimeoutOverrideSeconds: Math.max(1, Math.ceil(timeoutMs / 1e3)),\\n\\t\\t\\t\\t\\tonAgentRunStart: (runId) => {';const files=fs.readdirSync(dir).filter(n=>/\\.js$/.test(n));let patched=0;for(const file of files){const p=path.join(dir,file);let s='';try{s=fs.readFileSync(p,'utf8');}catch{continue;}if(s.includes(to)||!s.includes(from))continue;s=s.replace(from,to);fs.writeFileSync(p,s);patched++;}if(!patched){process.exit(0);}"`;
253
253
 
@@ -295,7 +295,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
295
295
  ' echo "[entrypoint] warning: failed to clone plugin $spec"',
296
296
  ' fi',
297
297
  ' ;;',
298
- ' *) openclaw plugins install "$spec" 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec" ;;',
298
+ ' *) openclaw plugins install "$spec" --acknowledge-clawhub-risk 2>/dev/null || echo "[entrypoint] warning: failed to install plugin $spec" ;;',
299
299
  ' esac',
300
300
  '}',
301
301
  'ensure_skill() {',
@@ -305,7 +305,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
305
305
  ' return 0',
306
306
  ' fi',
307
307
  ' echo "[entrypoint] skill $id missing; installing"',
308
- ' openclaw skills install "$id" 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
308
+ ' openclaw skills install "$id" --acknowledge-clawhub-risk 2>/dev/null || echo "[entrypoint] warning: failed to install skill $id"',
309
309
  '}',
310
310
  'echo "[entrypoint] ensuring runtime assets, then starting gateway"',
311
311
  ];
@@ -320,7 +320,7 @@ if(touched){console.log('[patch-9router] Applied Codex compatibility patch.');}e
320
320
  // install when extensions/zalo-connect already exists, so restarts never re-download
321
321
  // and a reconnect never reinstalls. No dist patching, no mentions.js, no watchdog
322
322
  // hacks: sticker/mention/reaction are native ZaloConnect actions.
323
- const zaloConnectSpec = common.ZALO_CONNECT_PLUGIN_SPEC || 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.0';
323
+ const zaloConnectSpec = common.ZALO_CONNECT_PLUGIN_SPEC || 'https://github.com/tuanminhhole/openclaw-zalo-connect.git#v3.0.1';
324
324
  runtimeParts.push(`ensure_plugin zalo-connect "${zaloConnectSpec}"`);
325
325
  }
326
326
  runtimeParts.push('openclaw gateway run');
@@ -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 || {};
@@ -899,12 +953,11 @@ function botListPanel(bots) {
899
953
  const role = (b.role || b.desc || b.description || '').trim() || t('Tr\u1ee3 l\u00fd OpenClaw','OpenClaw assistant');
900
954
  const isZalo = b.channel === 'zalo-personal';
901
955
  const health = isZalo ? zaloAccountHealth(b) : null;
902
- const quickBadge = health?.running ? `<span class="zalo-quick-badge">${t('Đã kết nối','Connected')}</span>` : '';
903
- const connection = !health ? t('Chưa rõ','Unknown') : health.running ? t('Hoạt động','Active') : health.lastError ? t('Mất kết nối','Disconnected') : t('Đang kết nối','Connecting');
956
+ const connection = !health ? t('Chưa rõ','Unknown') : health.running ? t('Đã kết nối','Connected') : health.lastError ? t('Mất kết nối','Disconnected') : t('Đang kết nối','Connecting');
904
957
  const login = !health ? t('Chưa rõ','Unknown') : health.sessionSaved ? t('Đã đăng nhập','Logged in') : t('Chưa đăng nhập','Not logged in');
905
958
  const connectionTone = health?.running ? 'ok' : health?.lastError ? 'bad' : 'warn';
906
959
  const loginTone = health?.sessionSaved ? 'ok' : health ? 'bad' : 'warn';
907
- return `<article class="bot-item ${state.activeBotId===b.id?'active':''}" data-bot-id="${escapeHtml(b.id)}"><div class="bot-item-actions"><button class="bot-edit" data-edit-bot="${escapeHtml(b.id)}" title="${t('Sửa bot','Edit bot')}" aria-label="${t('Sửa bot','Edit bot')}">${actionIcon('edit')}</button><button class="bot-delete" data-delete-bot="${escapeHtml(b.id)}" title="${t('X\u00f3a bot','Delete bot')}" aria-label="${t('X\u00f3a bot','Delete bot')}">&times;</button></div><div class="bot-item-title"><b>${escapeHtml(b.name)}</b>${quickBadge}</div><small title="${escapeHtml(role)}">${escapeHtml(role)}</small>${isZalo ? `<div class="zalo-bot-health"><div><span>${t('Kết nối','Connection')}</span><em class="${connectionTone}">${connection}</em></div><div><span>${t('Đăng nhập','Login')}</span><em class="${loginTone}">${login}</em></div></div>` : ''}</article>`;
960
+ return `<article class="bot-item ${state.activeBotId===b.id?'active':''}" data-bot-id="${escapeHtml(b.id)}"><div class="bot-item-actions"><button class="bot-edit" data-edit-bot="${escapeHtml(b.id)}" title="${t('Sửa bot','Edit bot')}" aria-label="${t('Sửa bot','Edit bot')}">${actionIcon('edit')}</button><button class="bot-delete" data-delete-bot="${escapeHtml(b.id)}" title="${t('X\u00f3a bot','Delete bot')}" aria-label="${t('X\u00f3a bot','Delete bot')}">&times;</button></div><div class="bot-item-title"><b>${escapeHtml(b.name)}</b></div><small title="${escapeHtml(role)}">${escapeHtml(role)}</small>${isZalo ? `<div class="zalo-bot-health"><div><span>${t('Kết nối','Connection')}</span><em class="${connectionTone}">${connection}</em></div><div><span>${t('Đăng nhập','Login')}</span><em class="${loginTone}">${login}</em></div></div>` : ''}</article>`;
908
961
  });
909
962
  listItems.push(`
910
963
  <article class="bot-item bot-create-card" data-bot-modal="open">
@@ -1127,8 +1180,16 @@ function wireTab() {
1127
1180
  localStorage.setItem('openclaw-'+btn.dataset.pref, btn.dataset.value);
1128
1181
  const main = $('#app-main-content');
1129
1182
  if (main) main.remove();
1130
- 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();
1131
1191
  });
1192
+ { const tzSel = $('#tz-select'); if (tzSel) tzSel.onchange = () => { state.tz = tzSel.value; localStorage.setItem('openclaw-tz', tzSel.value); }; }
1132
1193
  document.querySelectorAll('[data-tab-jump]').forEach(btn => btn.onclick = () => { state.tab = btn.dataset.tabJump; render(); });
1133
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)); });
1134
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)); });
@@ -1394,6 +1455,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1394
1455
  os: form.querySelector('[name="os"]')?.value || state.installDraft?.os || state.os,
1395
1456
  mode: form.querySelector('[name="mode"]')?.value || state.installTab || state.installDraft?.mode || state.mode,
1396
1457
  projectDir: form.querySelector('[name=\"projectDir\"]')?.value || '',
1458
+ userTimezone: state.tz,
1397
1459
  };
1398
1460
  if (!body.projectDir) throw new Error(t('Chưa có đường dẫn project','Missing project path'));
1399
1461
  await api('/api/install', { method: 'POST', body });
@@ -1430,6 +1492,7 @@ document.querySelectorAll('[data-project-pick-folder]').forEach(btn => btn.oncli
1430
1492
  const body = Object.fromEntries(fd.entries());
1431
1493
  body.projectDir = activeProjectDir();
1432
1494
  body.channel = state.botChannel || 'telegram';
1495
+ body.userTimezone = state.tz;
1433
1496
  if (body.channel === 'zalo-personal') {
1434
1497
  state.botModalOpen = false;
1435
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.0",
3
+ "version": "5.13.4",
4
4
  "description": "Interactive CLI installer for OpenClaw Bot",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {