diff --git a/DEVLOG.md b/DEVLOG.md index 923f3c0..3701fba 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,7 +1,40 @@ # OpenClaw Manager — 开发日志 > 最后更新:2026-02-27 -> 当前版本:v0.6.6 +> 当前版本:v0.7.0 + +--- + +## v0.7.0 更新日志(2026-02-27) + +### New Features + +**Dashboard Redesign — Circular Gauge System Health** +- CPU%, RAM%, DISK% displayed as SVG circular gauges with colour-coded arcs (green < 70%, amber < 90%, red ≥ 90%) +- System info card: hostname, OS, Node.js, CPU model, cores, uptime, load average (1/5/15 min) +- Agent overview: separate main agent count and sub-agent count (plus total) +- Auto-refresh toggle: when enabled, dashboard polls `/api/dashboard` every 10 seconds +- Backend now measures CPU usage via `os.cpus()` delta sampling (200ms interval) and returns `loadAvg` from `os.loadavg()` + +**Sub-Agent Creation Flow — Step-by-Step Guide Rewrite** +- Expanded from 3 steps to 5 detailed steps covering the full BotFather → Telegram Group → OCM workflow +- Step 1: Create new Bot via `/newbot` in BotFather +- Step 2: Disable Group Privacy via `/mybots` → Bot Settings → Group Privacy → Turn off +- Step 3: Create Telegram group, add Bot (with security warning) +- Step 4: Get Group ID from gateway logs +- Step 5: Fill in the form +- New "Your Telegram User ID" input field — auto-writes to `channels.telegram.allowFrom` whitelist on creation +- Security warning banner: "Do NOT add other people to this group" with explanation about API cost risks + +**Setup Page — English Localization** +- All Chinese text in the initial directory selection page translated to English +- Includes: title, description, labels, placeholders, error messages, button text + +### Technical Notes + +- **CPU usage measurement:** Two `os.cpus()` snapshots 200ms apart, calculating idle-to-total ratio across all cores +- **Gauge rendering:** Pure SVG arcs with `stroke-dasharray` animation, no external libraries. 270° arc (gap at bottom), colour transitions via `gaugeColor()` function +- **allowFrom auto-config:** `POST /api/agents` now accepts optional `telegramUserId` field (numeric string), appends to `channels.telegram.allowFrom[]` if not already present --- diff --git a/openclaw-manager.js b/openclaw-manager.js index 3383b77..313e647 100644 --- a/openclaw-manager.js +++ b/openclaw-manager.js @@ -1,6 +1,6 @@ #!/usr/bin/env node // ================================================================ -// OpenClaw Manager v0.6.5 +// OpenClaw Manager v0.7.0 // 跨平台本地管理工具 (Windows / macOS / Linux) // // 用法: @@ -24,7 +24,7 @@ const SCRIPT_DIR = __dirname; const MANAGER_CONFIG = path.join(SCRIPT_DIR, 'manager-config.json'); let PORT = 3333; let HOST = '0.0.0.0'; -const APP_VERSION = '0.6.6'; +const APP_VERSION = '0.7.0'; // --port 参数 const portIdx = process.argv.indexOf('--port'); if (portIdx !== -1 && process.argv[portIdx + 1]) PORT = parseInt(process.argv[portIdx + 1]) || 3333; @@ -456,7 +456,7 @@ async function handleApi(req, res, urlObj, body) { // POST /api/agents — create sub-agent (shares parent bot) if (method === 'POST' && pathname === '/api/agents') { - const { agentId, displayName, groupId, workspaceFolder, model, purpose, personality, initialMemory, parentAgentId } = body; + const { agentId, displayName, groupId, workspaceFolder, model, purpose, personality, initialMemory, parentAgentId, telegramUserId } = body; if (!agentId || !/^[a-zA-Z0-9_-]+$/.test(agentId)) { res.writeHead(400); res.end(JSON.stringify({ error: 'Agent ID must contain only alphanumeric characters, underscores, or dashes' })); return; } @@ -495,6 +495,14 @@ async function handleApi(req, res, urlObj, body) { if (!cfg.channels.telegram) cfg.channels.telegram = {}; if (!cfg.channels.telegram.groups) cfg.channels.telegram.groups = {}; cfg.channels.telegram.groups[gid] = { requireMention: false }; + // Add telegramUserId to allowFrom whitelist if provided + if (telegramUserId && /^\d+$/.test(String(telegramUserId).trim())) { + const uid = parseInt(String(telegramUserId).trim()); + if (!cfg.channels.telegram.allowFrom) cfg.channels.telegram.allowFrom = []; + if (!cfg.channels.telegram.allowFrom.includes(uid)) { + cfg.channels.telegram.allowFrom.push(uid); + } + } const bakPath = await writeConfig(cfg, 'create'); await fsp.mkdir(wsPath, { recursive: true }); await fsp.mkdir(path.join(wsPath, 'memory'), { recursive: true }); @@ -1247,6 +1255,25 @@ async function handleApi(req, res, urlObj, body) { const cpus = os.cpus(); const cpuModel = cpus.length ? cpus[0].model.trim() : 'Unknown'; const cpuCores = cpus.length; + const loadAvg = os.loadavg(); // [1min, 5min, 15min] + + // CPU usage % (snapshot via /proc/stat or fallback to loadavg) + let cpuPercent = null; + try { + // Quick estimate: sum idle vs total across all cores from a snapshot + const c1 = os.cpus(); + await new Promise(r => setTimeout(r, 200)); + const c2 = os.cpus(); + let idleDiff = 0, totalDiff = 0; + for (let i = 0; i < c2.length; i++) { + const t1 = c1[i].times, t2 = c2[i].times; + const total1 = t1.user + t1.nice + t1.sys + t1.idle + t1.irq; + const total2 = t2.user + t2.nice + t2.sys + t2.idle + t2.irq; + idleDiff += (t2.idle - t1.idle); + totalDiff += (total2 - total1); + } + cpuPercent = totalDiff > 0 ? Math.round((1 - idleDiff / totalDiff) * 100) : 0; + } catch (_) {} // Disk usage (best-effort, works on macOS/Linux) let diskTotal = 0, diskUsed = 0, diskFree = 0; @@ -1295,9 +1322,21 @@ async function handleApi(req, res, urlObj, body) { gatewayPing = code > 0 && code < 500; } catch (_) {} - // Agent count & last activity - let agentCount = 0; + // Agent count (main vs sub) & last activity + let agentCount = 0, mainAgentCount = 0, subAgentCount = 0; let lastActivity = null; + try { + // Count main vs sub from config + if (cfg && cfg.agents && cfg.agents.list && cfg.bindings) { + const bindings = cfg.bindings || []; + const accounts = cfg.channels?.telegram?.accounts || {}; + cfg.agents.list.forEach(a => { + const isMain = a.id === 'main'; + const botBinding = bindings.find(b => b.agentId === a.id && b.match?.accountId && !b.match?.peer); + if (isMain || botBinding) mainAgentCount++; else subAgentCount++; + }); + } + } catch (_) {} try { const sessionsBase = path.join(OPENCLAW_DIR, 'agents'); const agentDirs = await fsp.readdir(sessionsBase); @@ -1322,9 +1361,9 @@ async function handleApi(req, res, urlObj, body) { res.writeHead(200); res.end(JSON.stringify({ ok: true, - system: { hostname, platform, nodeVer, cpuModel, cpuCores, uptime: sysUptime, totalMem, freeMem, diskTotal, diskUsed, diskFree, dirSize }, + system: { hostname, platform, nodeVer, cpuModel, cpuCores, uptime: sysUptime, totalMem, freeMem, diskTotal, diskUsed, diskFree, dirSize, cpuPercent, loadAvg }, gateway: { status: gatewayRunning, pid: gatewayPid, port: gatewayPort, ping: gatewayPing }, - agents: { count: agentCount, lastActivity: lastActivity ? lastActivity.toISOString() : null }, + agents: { count: agentCount, mainCount: mainAgentCount, subCount: subAgentCount, lastActivity: lastActivity ? lastActivity.toISOString() : null }, ocmVersion: APP_VERSION, serverTime: now, })); @@ -1383,12 +1422,12 @@ async function handleApi(req, res, urlObj, body) { } res.writeHead(404); - res.end(JSON.stringify({ error: '未知 API 路径: ' + pathname })); + res.end(JSON.stringify({ error: 'Unknown API path: ' + pathname })); } // ── 安装向导 HTML ────────────────────────────────────────────── const SETUP_HTML = ` -
首次运行,请指定你的 OpenClaw 数据目录(包含 openclaw.json 的文件夹)。
- - -~/.openclawC:\\Users\\yourname\\.openclawFirst time setup — please specify your OpenClaw data directory (the folder containing openclaw.json).
+ + +~/.openclawC:\\Users\\yourname\\.openclaw/newbot and follow prompts to name your Bot',
'guide.agent.s3':'Copy the Bot Token from BotFather and paste below',
'guide.agent.s4':'Send /setprivacy → select your Bot → click Disable (allow Bot to read group messages)',
- 'guide.sub.s1':'Add the Bot to your target Telegram group',
- 'guide.sub.s2':'Send a message in the group, find peer.id (negative number) in gateway logs',
- 'guide.sub.s3':'Fill in the Group ID and Agent config below',
+ 'guide.sub.s1':'在 BotFather 发送 /newbot 创建新 Bot,获取 Bot Token',
+ 'guide.sub.s2':'在 BotFather 发送 /mybots → 选择 Bot → Bot Settings → Group Privacy → Turn off',
+ 'guide.sub.s3':'在 Telegram 创建新群组,将 Bot 加入群组(不要加其他人)',
+ 'guide.sub.s4':'在群内发一条消息,从 gateway 日志中找到 peer.id(负数)',
+ 'guide.sub.s5':'填写下方表单创建 Sub-Agent',
+ 'guide.sub.warn':'⚠️ 安全提示:请勿将其他人加入此群组,只有你和 Bot 应在群内。否则其他人也能与 Bot 对话并产生 API 费用。',
+ 'wiz.telegramId':'你的 Telegram User ID','wiz.telegramIdHint':'💡 可通过 @userinfobot 获取,填写后自动配置 allowFrom 白名单','wiz.telegramIdPh':'例如: 123456789',
'agents.empty':'暂无 Agent','agents.main':'主 Agent','agents.bound':'已绑群',
'agents.saveModel':'保存模型','agents.viewFiles':'查看文件',
'agents.defaultModel':'使用全局默认','agents.custom':'自定义','agents.noModel':'默认',
@@ -2274,9 +2335,13 @@ const I18N = {
'guide.agent.s2':'Send /newbot and follow prompts to name your Bot',
'guide.agent.s3':'Copy the Bot Token from BotFather and paste below',
'guide.agent.s4':'Send /setprivacy → select your Bot → click Disable (allow Bot to read group messages)',
- 'guide.sub.s1':'Add the Bot to your target Telegram group',
- 'guide.sub.s2':'Send a message in the group, find peer.id (negative number) in gateway logs',
- 'guide.sub.s3':'Fill in the Group ID and Agent config below',
+ 'guide.sub.s1':'Send /newbot to BotFather to create a new Bot and get the Bot Token',
+ 'guide.sub.s2':'Send /mybots to BotFather → select your Bot → Bot Settings → Group Privacy → Turn off',
+ 'guide.sub.s3':'Create a new Telegram group, add the Bot to the group (do NOT add anyone else)',
+ 'guide.sub.s4':'Send a message in the group, find peer.id (negative number) in gateway logs',
+ 'guide.sub.s5':'Fill in the form below to create the Sub-Agent',
+ 'guide.sub.warn':'⚠️ Security: Do NOT add other people to this group. Only you and the Bot should be in the group. Otherwise others can chat with the Bot and incur API costs.',
+ 'wiz.telegramId':'Your Telegram User ID','wiz.telegramIdHint':'💡 Get it from @userinfobot — auto-configures allowFrom whitelist','wiz.telegramIdPh':'e.g. 123456789',
'agents.empty':'No Agents','agents.main':'Main Agent','agents.bound':'Bound',
'agents.saveModel':'Save Model','agents.viewFiles':'View Files',
'agents.defaultModel':'Use Global Default','agents.custom':'custom','agents.noModel':'Default',
@@ -2414,24 +2479,56 @@ async function loadAll(){ await Promise.all([loadAgents(), loadModels(), loadCha
// ── Dashboard ─────────────────────────────────────────────────
let dashLoaded=false;
+let dashRefreshTimer=null;
function fmtBytes(b){if(!b||b<=0)return '—';const u=['B','KB','MB','GB','TB'];let i=0;while(b>=1024&&i