8 Commits
Author SHA1 Message Date
taodeng 6fe2e2bd3a feat: release v0.7.1 telegram guidance + CLI model dropdown source 2026-02-28 07:14:51 +10:00
taodeng cedade141b updated subagent logic 2026-02-27 19:53:12 +10:00
taodeng 154b863f78 Dashboard redesign 2026-02-27 18:05:24 +10:00
taodengandClaude Opus 4.6 07d816053f docs: update DEVLOG next steps for v0.7+ roadmap
Check off completed v0.6.x items and add new planned features:
agent inline editing, gateway restart button, settings tab, bot connectivity test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:09:06 +00:00
taodengandClaude Opus 4.6 6346ea2748 fix: main agent always root in agent tree + accountId inference (v0.6.6)
main agent in OpenClaw has no explicit binding - it's the default catch-all.
Previous code required explicit binding with accountId && !peer to detect
root agents, causing main and its sub-agents to appear as orphan independents.

Now main is always treated as root, with accountId inferred from first
unclaimed telegram account in channels.telegram.accounts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:04:28 +00:00
taodengandClaude Opus 4.6 69269963d0 fix: Add Agent creates runtime dirs + SOUL.md personality support
- POST /api/agents/bot now creates agents/<id>/sessions/ runtime directory
- POST /api/agents (sub-agent) also creates agents/<id>/sessions/
- Add Agent form now includes Purpose and Personality fields
- Personality keywords generate structured SOUL.md, empty uses default growth prompt
- Notes now remind to restart gateway for new bots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:15:29 +00:00
taodengandClaude Opus 4.6 c6150d117b fix: agents side-by-side layout + main workspace display (v0.6.5)
- Main agents displayed horizontally side-by-side (flex row)
- Sub-agents drop below their respective parent agent
- Main agent now shows defaults.workspace path
- All agents get "View Files" button regardless of root/sub status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:04:48 +00:00
taodengandClaude Opus 4.6 8eb9801f0f fix: agent tree shows each bot as independent root + new layout (v0.6.5)
- Each agent with hasOwnBot is rendered as independent tree root (not all under "main")
- Layout changed: buttons centered top, agent tree below (max 720px centered)
- Added expand/collapse toggle at top of sub-agent branch line
- API returns accountId/parentAccountId for proper tree grouping
- Event delegation for agent tree actions (data-action attributes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:52:23 +00:00
4 changed files with 485 additions and 142 deletions
+96 -3
View File
@@ -1,7 +1,92 @@
# OpenClaw Manager — 开发日志
> 最后更新:2026-02-26
> 当前版本:v0.6.5
> 最后更新:2026-02-27
> 当前版本:v0.7.1
---
## v0.7.1 更新日志(2026-02-27
### New Features
**Dashboard Guidance Block (Telegram-first usage + safety)**
- Added a concise guidance card under Dashboard explaining OCM's intended Telegram workflow
- Clarifies prerequisites: users should already have basic OpenClaw CLI experience
- Clarifies purpose: OCM mainly helps visualize and update `openclaw.json` for easier main-agent/sub-agent management and multi-tree structures
- Added explicit Telegram security checklist:
- BotFather `Allow Groups = ON`
- BotFather `Group Privacy = OFF`
- Keep each group private (only you + agent/sub-agents), do not invite others
### Improvements
**Model Dropdown Source — only `openclaw models list`**
- Model dropdown options are now sourced from real CLI output (`openclaw models list`) instead of built-in/static lists or config-only additions
- Applies to all relevant selectors: agent/sub-agent creation forms, agent inline model switcher, primary model selector, and fallback picker
- Added warning message when model list loading/parsing fails
### Technical Notes
- Added model ID parser for CLI output and short-term cache for model list fetches
- `GET /api/models` now returns:
- `knownModels` from CLI
- `modelListError` for UI warning display
- Startup script banners updated to `v0.7.1` (`start.sh` / `start.bat`)
---
## 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
### Bug Fixes
**Agent tree: sub-agents not grouped under main (old config format)**
- **Symptom**: On machines with openclaw.json created before OCM v0.6 multi-bot support, all sub-agents displayed as independent cards instead of nested under main
- **Root cause**: Old-format bindings lack `accountId` field in sub-agent entries. The tree builder relied on `parentAccountId` to match sub-agents to roots, but old bindings only had `channel` + `peer` — no `accountId`. So `parentAccountId` was null and no parent match was found
- **Fix**: Fallback inference — if a sub-agent has a peer binding but no `accountId`, automatically infer `parentAccountId` from main's binding (`accountId`) or fall back to `'default'`
### 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
- **Old-config compatibility:** Sub-agent bindings without `accountId` are now auto-assigned to the main agent's bot, maintaining backward compatibility with configs created before multi-bot support
---
## v0.6.6 更新日志(2026-02-27
### Bug Fixes
**Agent tree: main agent not detected as root**
- **Symptom**: Sub-agents displayed as independent agents instead of under their parent. All agents appeared as orphan roots.
- **Root cause**: `main` agent in OpenClaw is the default catch-all and typically has NO explicit binding in `openclaw.json`. The `hasOwnBot` detection required an explicit binding with `accountId && !peer`, so `main` was classified as a non-root. Without `main` being a root, sub-agents' `parentAccountId` had no matching root to link to.
- **Fix**:
- `main` is now always treated as a root agent regardless of binding existence
- `main`'s `accountId` is inferred from `channels.telegram.accounts`: first unclaimed account (not explicitly bound to another agent), or first account as fallback
- Sub-agents with `parentAccountId` matching `main`'s inferred `accountId` now correctly appear under `main` in the tree
---
@@ -133,13 +218,21 @@
- **`assertBrowserScriptSyntax()`** 在启动时检查 MAIN_HTML_SCRIPT 的 evaluated 值
- **OpenClaw config 多 bot 格式**`channels.telegram.accounts.<id>.botToken`binding 用 `accountId` 路由
### 下一步(v0.6.2+
### 下一步(v0.7+
- [x] Dashboard 首页 tab(系统信息 + OpenClaw health 状态)— done in v0.6.5
- [x] HTTP 响应加 `Cache-Control: no-store` + version-based cache busting — done in v0.6.5
- [x] 彻底删除旧 `/api/agents/main` 端点残留(确认已清除)— confirmed in v0.6.5
- [x] Agent tree: independent roots per bot + side-by-side layout — done in v0.6.5
- [x] Agent tree: expand/collapse toggle for sub-agents — done in v0.6.5
- [x] Add Agent: creates `agents/<id>/sessions/` runtime dirs + SOUL.md personality — done in v0.6.5
- [x] Agent tree: main agent always root with inferred accountId — done in v0.6.6
- [ ] Dashboard: auto-refresh every 30s when tab is active
- [ ] Dashboard: OpenClaw version display (from `openclaw --version`)
- [ ] Agent page: edit agent name, purpose, personality inline
- [ ] Agent page: restart gateway button with status feedback
- [ ] Settings tab: edit openclaw.json key fields via form (model, workspace defaults)
- [ ] Telegram bot connectivity test button (ping bot API from server)
- [ ] DEVLOG.md 中文 → 逐步迁移为英文
---
+385 -135
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// ================================================================
// OpenClaw Manager v0.6.5
// OpenClaw Manager v0.7.1
// 跨平台本地管理工具 (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.5';
const APP_VERSION = '0.7.1';
// --port 参数
const portIdx = process.argv.indexOf('--port');
if (portIdx !== -1 && process.argv[portIdx + 1]) PORT = parseInt(process.argv[portIdx + 1]) || 3333;
@@ -197,6 +197,37 @@ function runOpenclawCmd(args) {
});
}
function parseModelIdsFromCliOutput(raw) {
if (!raw) return [];
const set = new Set();
const isModelId = (s) => /^[a-z0-9][a-z0-9_-]*(?:\/[A-Za-z0-9._:-]+)+$/.test(s);
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) continue;
const normalized = trimmed.replace(/^[-*]\s+/, '');
const first = normalized.split(/\s+/)[0].replace(/^`|`$/g, '');
if (isModelId(first)) set.add(first);
}
return Array.from(set).sort((a, b) => a.localeCompare(b));
}
let MODEL_LIST_CACHE = { ts: 0, knownModels: [], error: '' };
async function getKnownModelsFromCli() {
const now = Date.now();
if (now - MODEL_LIST_CACHE.ts < 30000) return MODEL_LIST_CACHE;
try {
const out = await runOpenclawCmd('models list');
const ids = parseModelIdsFromCliOutput(out);
const knownModels = ids.map(id => ({ id, label: id, group: id.split('/')[0] || 'other' }));
const error = knownModels.length ? '' : 'No model IDs found in `openclaw models list` output';
MODEL_LIST_CACHE = { ts: now, knownModels, error };
return MODEL_LIST_CACHE;
} catch (e) {
MODEL_LIST_CACHE = { ts: now, knownModels: [], error: e.message || 'openclaw models list failed' };
return MODEL_LIST_CACHE;
}
}
// 过滤 ANSI 终端控制码(光标移动、清行、颜色等)
function stripAnsi(str) {
return str
@@ -345,15 +376,44 @@ async function handleApi(req, res, urlObj, body) {
const defaults = cfg.agents?.defaults || {};
const bindings = cfg.bindings || [];
const groups = cfg.channels?.telegram?.groups || {};
const defaultWorkspace = defaults.workspace || null;
// Build set of accountIds explicitly claimed by non-main agents (via non-peer binding)
const telegramAccounts = Object.keys(cfg.channels?.telegram?.accounts || {});
const claimedAccounts = new Set(
bindings.filter(b => b.agentId !== 'main' && b.match?.accountId && !b.match?.peer).map(b => b.match.accountId)
);
// For main agent without explicit binding, infer its accountId from first unclaimed telegram account
const mainInferredAccountId = telegramAccounts.find(a => !claimedAccounts.has(a)) || telegramAccounts[0] || null;
const enriched = list.map(a => {
const binding = bindings.find(b => b.agentId === a.id && b.match?.peer?.kind === 'group');
const groupId = binding?.match?.peer?.id || null;
const modelVal = a.model?.primary || (typeof a.model === 'string' ? a.model : null);
// Check if agent has its own bot account (binding with accountId but no peer, or accountId matching agent ID)
// Check if agent has its own bot account (binding with accountId but no peer)
const botBinding = bindings.find(b => b.agentId === a.id && b.match?.accountId && !b.match?.peer);
const hasOwnBot = botBinding ? true : false;
return { ...a, groupId, requireMention: groupId ? (groups[groupId]?.requireMention ?? true) : null,
effectiveModel: modelVal || defaults.model?.primary || '默认', hasOwnBot };
// 'main' is always a root agent even without explicit binding
const isMain = a.id === 'main';
const hasOwnBot = botBinding ? true : isMain;
const accountId = botBinding?.match?.accountId || (isMain ? mainInferredAccountId : null);
// For sub-agents (with peer match), find which accountId they belong to
const parentBinding = bindings.find(b => b.agentId === a.id && b.match?.accountId);
// Fallback: if sub-agent has peer binding but no accountId, AND there's only one bot (old single-bot config),
// infer parentAccountId from main. If multiple bots exist, leave as orphan (can't guess which bot it belongs to).
let parentAccountId = parentBinding?.match?.accountId || null;
if (!parentAccountId && !hasOwnBot) {
const hasPeerBinding = bindings.find(b => b.agentId === a.id && b.match?.peer);
if (hasPeerBinding) {
const rootBindings = bindings.filter(b => b.match?.accountId && !b.match?.peer);
const rootCount = rootBindings.length + (list.some(x => x.id === 'main') && !rootBindings.some(b => b.agentId === 'main') ? 1 : 0);
if (rootCount <= 1) {
const mainBinding = rootBindings.find(b => b.agentId === 'main') || rootBindings[0];
parentAccountId = mainBinding?.match?.accountId || mainInferredAccountId || 'default';
}
}
}
// Workspace: explicit per-agent, or defaults.workspace for main
const workspace = a.workspace || (isMain ? defaultWorkspace : null);
return { ...a, workspace, groupId, requireMention: groupId ? (groups[groupId]?.requireMention ?? true) : null,
effectiveModel: modelVal || defaults.model?.primary || '默认', hasOwnBot, accountId, parentAccountId };
});
res.writeHead(200);
res.end(JSON.stringify({ agents: enriched, defaults }));
@@ -362,7 +422,7 @@ async function handleApi(req, res, urlObj, body) {
// POST /api/agents/bot — create agent with its own bot token
if (method === 'POST' && pathname === '/api/agents/bot') {
const { botToken, agentId, name, model, workspace } = body;
const { botToken, agentId, name, model, workspace, purpose, personality } = body;
if (!botToken || !botToken.trim()) {
res.writeHead(400); res.end(JSON.stringify({ error: 'Bot Token is required' })); return;
}
@@ -418,16 +478,21 @@ async function handleApi(req, res, urlObj, body) {
await fsp.mkdir(wsPath, { recursive: true });
await fsp.mkdir(path.join(wsPath, 'memory'), { recursive: true });
const agentName = name || agentId;
await fsp.writeFile(path.join(wsPath, 'SOUL.md'), generateSoulMd(agentName, '', ''), 'utf8');
await fsp.writeFile(path.join(wsPath, 'SOUL.md'), generateSoulMd(agentName, purpose || '', personality || ''), 'utf8');
await fsp.writeFile(path.join(wsPath, 'MEMORY.md'), generateMemoryMd(agentName, ''), 'utf8');
// Create agents/<id>/ runtime directory (required by OpenClaw gateway)
const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId);
await fsp.mkdir(agentRuntimeDir, { recursive: true });
await fsp.mkdir(path.join(agentRuntimeDir, 'sessions'), { recursive: true });
res.writeHead(200);
res.end(JSON.stringify({
ok: true, agentId, workspacePath: wsPath, configBackup: bakPath,
notes: [
'Agent created with its own bot token',
'Workspace directory created with SOUL.md and MEMORY.md',
'Runtime directory created at agents/' + agentId + '/',
'Configuration updated and backed up',
'Changes take effect in ~300ms without restart'
'Restart gateway to load new bot: openclaw gateway restart'
]
}));
return;
@@ -435,7 +500,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;
}
@@ -474,19 +539,31 @@ 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 });
const name = displayName || agentId;
await fsp.writeFile(path.join(wsPath, 'SOUL.md'), generateSoulMd(name, purpose, personality), 'utf8');
await fsp.writeFile(path.join(wsPath, 'MEMORY.md'), generateMemoryMd(name, initialMemory), 'utf8');
// Create agents/<id>/ runtime directory (required by OpenClaw gateway)
const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId);
await fsp.mkdir(agentRuntimeDir, { recursive: true });
await fsp.mkdir(path.join(agentRuntimeDir, 'sessions'), { recursive: true });
res.writeHead(200);
res.end(JSON.stringify({ ok: true, agentId, workspacePath: wsPath, configBackup: bakPath,
notes: [
'Sub-agent created and shares parent bot',
'Workspace directory created with SOUL.md and MEMORY.md',
'Workspace and runtime directories created',
'Configuration updated and backed up',
'Changes take effect in ~300ms without restart'
'Restart gateway to apply: openclaw gateway restart'
],
}));
return;
@@ -582,13 +659,15 @@ async function handleApi(req, res, urlObj, body) {
// GET /api/models
if (method === 'GET' && pathname === '/api/models') {
const cfg = await readConfig();
const modelList = await getKnownModelsFromCli();
res.writeHead(200);
res.end(JSON.stringify({
models: cfg.agents?.defaults?.models || {},
authProfiles: cfg.auth?.profiles || {},
primaryModel: cfg.agents?.defaults?.model?.primary || '',
fallbacks: cfg.agents?.defaults?.model?.fallbacks || [],
knownModels: KNOWN_MODELS,
knownModels: modelList.knownModels || [],
modelListError:modelList.error || '',
authProviders: AUTH_PROVIDERS,
}));
return;
@@ -1222,6 +1301,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;
@@ -1270,9 +1368,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);
@@ -1297,9 +1407,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,
}));
@@ -1358,12 +1468,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 = `<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>OpenClaw Manager - 初始设置</title>
<html lang="en"><head><meta charset="UTF-8"><title>OpenClaw Manager - Setup</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0f1117;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;}
@@ -1381,23 +1491,23 @@ const SETUP_HTML = `<!DOCTYPE html>
</style></head>
<body><div class="box">
<h1>🦀 OpenClaw Manager</h1>
<p>首次运行,请指定你的 OpenClaw 数据目录(包含 openclaw.json 的文件夹)。</p>
<label>OpenClaw 目录路径</label>
<input id="dir" type="text" placeholder="例如: /Users/yourname/.openclaw ~/.openclaw">
<div class="hint">常见位置:<br>macOS / Linux<code>~/.openclaw</code><br>Windows<code>C:\\Users\\yourname\\.openclaw</code></div>
<p>First time setup — please specify your OpenClaw data directory (the folder containing openclaw.json).</p>
<label>OpenClaw Directory Path</label>
<input id="dir" type="text" placeholder="e.g. /Users/yourname/.openclaw or ~/.openclaw">
<div class="hint">Common locations:<br>macOS / Linux: <code>~/.openclaw</code><br>Windows: <code>C:\\Users\\yourname\\.openclaw</code></div>
<div class="err" id="err"></div>
<button onclick="save()">确认并进入</button>
<button onclick="save()">Confirm &amp; Enter</button>
</div>
<script>
async function save(){
const dir=document.getElementById('dir').value.trim();
const err=document.getElementById('err');
if(!dir){err.textContent='请填写目录路径';err.style.display='block';return;}
if(!dir){err.textContent='Please enter a directory path';err.style.display='block';return;}
try{
const r=await fetch('/api/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dir})});
const d=await r.json();
if(d.ok){location.reload();}else{err.textContent=d.error||'路径无效';err.style.display='block';}
}catch(e){err.textContent='请求失败:'+e.message;err.style.display='block';}
if(d.ok){location.reload();}else{err.textContent=d.error||'Invalid path';err.style.display='block';}
}catch(e){err.textContent='Request failed: '+e.message;err.style.display='block';}
}
document.getElementById('dir').addEventListener('keydown',e=>{if(e.key==='Enter')save();});
</script></body></html>`;
@@ -1466,12 +1576,11 @@ main { padding:20px; max-width:1280px; margin:0 auto; }
.inline-sel { background:var(--bg); border:1px solid var(--border); color:var(--text); border-radius:6px; padding:5px 8px; font-size:12px; }
.inline-sel:focus { outline:none; border-color:var(--accent); }
/* Agents split layout */
.agents-split { display:flex; gap:20px; height:calc(100vh - 160px); }
.agents-left { flex:0 0 40%; display:flex; flex-direction:column; gap:12px; overflow-y:auto; }
.agents-right { flex:1; overflow-y:auto; padding-right:4px; }
.agents-left-btns { display:flex; gap:8px; }
.agents-left-btns .btn-primary { flex:1; }
/* Agents layout — buttons top, tree below */
.agents-top-btns { display:flex; gap:10px; justify-content:center; margin-bottom:18px; }
.agents-tree-wrap { max-width:900px; margin:0 auto; overflow-y:auto; }
.agents-roots { display:flex; gap:16px; flex-wrap:wrap; }
.agents-roots > .agent-tree-root { flex:1; min-width:320px; }
/* Add form */
.add-form { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:16px; }
@@ -1488,7 +1597,12 @@ main { padding:20px; max-width:1280px; margin:0 auto; }
.agent-tree-root .tree-main:hover { border-color:var(--accent); }
.agent-tree-root .tree-main .tree-title { font-size:14px; font-weight:600; display:flex; align-items:center; gap:8px; }
.agent-tree-root .tree-main .tree-meta { font-size:11px; color:var(--muted); margin-top:4px; }
.tree-children { margin-left:20px; border-left:2px solid var(--border); padding-left:14px; margin-top:6px; }
.tree-children-wrap { position:relative; margin-left:20px; padding-left:14px; margin-top:6px; }
.tree-children-wrap::before { content:''; position:absolute; left:0; top:0; bottom:8px; width:2px; background:var(--border); }
.tree-toggle { position:absolute; left:-8px; top:-4px; width:18px; height:18px; border-radius:50%; background:var(--surface); border:1.5px solid var(--border); color:var(--muted); font-size:12px; line-height:15px; text-align:center; cursor:pointer; z-index:2; padding:0; }
.tree-toggle:hover { border-color:var(--accent); color:var(--accent); }
.tree-children { }
.tree-children.collapsed { display:none; }
.tree-child { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:10px 12px; margin-bottom:8px; }
.tree-child:hover { border-color:var(--accent); }
.tree-child .tree-title { font-size:13px; font-weight:600; display:flex; align-items:center; gap:6px; }
@@ -1529,9 +1643,27 @@ select option { background:var(--surface); }
.pw-toggle { position:absolute; right:10px; top:50%; transform:translateY(-50%); background:none; border:none; color:var(--muted); cursor:pointer; font-size:14px; padding:0; }
/* ── Dashboard ── */
.dash-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:16px; }
.dash-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }
.dash-header h2 { font-size:18px; font-weight:600; color:var(--text); margin:0; }
.dash-auto-refresh { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); }
.dash-auto-refresh label { cursor:pointer; display:flex; align-items:center; gap:6px; }
.dash-toggle { position:relative; width:36px; height:20px; }
.dash-toggle input { opacity:0; width:0; height:0; }
.dash-toggle .slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background:var(--border); border-radius:10px; transition:.3s; }
.dash-toggle .slider:before { position:absolute; content:""; height:14px; width:14px; left:3px; bottom:3px; background:#999; border-radius:50%; transition:.3s; }
.dash-toggle input:checked + .slider { background:var(--accent); }
.dash-toggle input:checked + .slider:before { transform:translateX(16px); background:#fff; }
.dash-gauges { display:flex; gap:20px; justify-content:center; flex-wrap:wrap; margin-bottom:24px; }
.dash-gauge-card { background:var(--surface); border:1px solid var(--border); border-radius:14px; padding:20px 24px; display:flex; flex-direction:column; align-items:center; min-width:140px; }
.dash-gauge-svg { width:110px; height:110px; }
.dash-gauge-label { font-size:12px; color:var(--muted); margin-top:8px; font-weight:500; }
.dash-sections { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:16px; }
.dash-card { padding:20px; }
.dash-card h3 { font-size:14px; font-weight:600; margin-bottom:14px; color:var(--text); }
.dash-notice { margin-top:18px; padding:16px 18px; }
.dash-notice h3 { font-size:14px; margin-bottom:10px; }
.dash-note-list { margin-left:16px; color:var(--muted); font-size:12px; line-height:1.7; }
.dash-note-list li { margin-bottom:4px; }
.dash-row { display:flex; justify-content:space-between; align-items:center; padding:6px 0; border-bottom:1px solid var(--border); font-size:12px; }
.dash-row:last-child { border-bottom:none; }
.dash-label { color:var(--muted); }
@@ -1540,10 +1672,6 @@ select option { background:var(--surface); }
.dash-indicator.running { background:#22c55e; box-shadow:0 0 6px rgba(34,197,94,.5); }
.dash-indicator.stopped { background:#ef4444; box-shadow:0 0 6px rgba(239,68,68,.5); }
.dash-indicator.unknown { background:#f59e0b; }
.dash-bar-wrap { width:100%; height:6px; background:var(--border); border-radius:3px; margin-top:4px; }
.dash-bar { height:100%; border-radius:3px; background:var(--accent); transition:width .3s; }
.dash-bar.warn { background:#f59e0b; }
.dash-bar.danger { background:#ef4444; }
/* ── Modal ── */
.backdrop { position:fixed; inset:0; background:rgba(0,0,0,.72); z-index:100; display:none; align-items:center; justify-content:center; }
@@ -1707,9 +1835,17 @@ const MAIN_HTML_BODY = String.raw`
<!-- ══ Dashboard ════════════════════════════════════════════ -->
<div class="panel active" id="panel-dashboard">
<div class="dash-grid" id="dashGrid">
<div class="dash-header">
<h2>Dashboard</h2>
<div class="dash-auto-refresh">
<label class="dash-toggle"><input type="checkbox" id="dashAutoRefresh" onchange="toggleDashRefresh(this.checked)"><span class="slider"></span></label>
<span>Auto-refresh</span>
</div>
</div>
<div class="dash-gauges" id="dashGauges"><div class="empty">Loading...</div></div>
<div class="dash-sections">
<div class="card dash-card" id="dashSystem">
<h3>🖥️ System</h3>
<h3>🖥️ System Info</h3>
<div class="dash-items" id="dashSysItems"><div class="empty">Loading...</div></div>
</div>
<div class="card dash-card" id="dashGateway">
@@ -1725,23 +1861,26 @@ const MAIN_HTML_BODY = String.raw`
<div class="dash-items" id="dashStorageItems"><div class="empty">Loading...</div></div>
</div>
</div>
<div class="card dash-notice">
<h3 data-i18n="dash.noticeTitle">📌 Telegram 使用说明(重要)</h3>
<ul class="dash-note-list">
<li data-i18n="dash.notice1">OCM 主要面向 Telegram 场景:通过群组绑定 Agent,让每个 Agent 拥有独立 Workspace / SOUL.md / MEMORY.md。</li>
<li data-i18n="dash.notice2">使用前请确保你已具备基本 OpenClaw 操作经验;OCM 主要负责可视化更新 openclaw.json,方便增删主 Agent 与 Sub-Agent,并支持多条 Agent 树。</li>
<li data-i18n="dash.notice3">BotFather 中请确认 Allow Groups = ON 且 Group Privacy = OFF,否则 Sub-Agent 可能无法入组或无法响应。</li>
</ul>
<div class="warn-box" style="margin-top:10px" data-i18n="dash.noticeWarn">⚠️ 强烈建议:每个 Agent 群组只保留你自己和该 Agent(或其 Sub-Agent)。不要邀请其他人,把每个组当作私聊空间。</div>
</div>
</div>
<!-- ══ Agents ════════════════════════════════════════════════ -->
<div class="panel" id="panel-agents">
<div class="agents-split">
<!-- Left: Add forms -->
<div class="agents-left">
<div class="agents-left-btns">
<button class="btn-primary" onclick="showAddForm('agent')" data-i18n="agents.addAgent"> Add Agent</button>
<button class="btn-primary" onclick="showAddForm('sub')" data-i18n="agents.addSub"> Add Sub-Agent</button>
</div>
<div id="addFormArea"></div>
</div>
<!-- Right: Agent tree -->
<div class="agents-right">
<div id="agentTree"><div class="empty" data-i18n="agents.empty">No Agents</div></div>
</div>
<div class="agents-top-btns">
<button class="btn-primary" onclick="showAddForm('agent')" data-i18n="agents.addAgent"> Add Agent</button>
<button class="btn-primary" onclick="showAddForm('sub')" data-i18n="agents.addSub"> Add Sub-Agent</button>
</div>
<div id="addFormArea"></div>
<div class="agents-tree-wrap">
<div id="agentTree"><div class="empty" data-i18n="agents.empty">No Agents</div></div>
</div>
</div>
@@ -1759,6 +1898,8 @@ const MAIN_HTML_BODY = String.raw`
<div class="panel" id="panel-models">
<div class="sec-hdr"><h2 data-i18n="models.title">模型管理</h2></div>
<p class="hint-text" style="margin-bottom:14px" data-i18n="models.hint">模型由 openclaw onboard 注册,此处管理主模型和 Fallback 链。</p>
<p class="hint-text" style="margin-top:-8px;margin-bottom:12px" data-i18n="models.onlyCliHint">模型下拉仅显示 openclaw models list 返回的模型。</p>
<div id="modelListWarn" class="warn-box" style="display:none;margin-bottom:10px"></div>
<div id="primaryModelWarn" class="warn-box" style="display:none;margin-bottom:10px"></div>
<div class="card" style="margin-bottom:12px">
<div class="card-row"><span style="font-size:13px;font-weight:600" data-i18n="models.primary">默认主模型</span><span class="badge main">primary</span></div>
@@ -2106,6 +2247,14 @@ const I18N = {
'channels.title':'Channel 绑定','channels.add':' 添加绑定','channels.hint':'管理 Agent 与频道/群组的绑定关系。绑定顺序决定优先级。',
'models.title':'模型管理','models.primary':'默认主模型','models.fallback':'Fallback 链',
'models.hint':'模型由 openclaw onboard 注册,此处管理主模型和 Fallback 链。',
'models.onlyCliHint':'模型下拉仅显示 openclaw models list 返回的模型。',
'models.modelListErr':'读取 openclaw models list 失败:',
'models.modelListEmpty':'未从 openclaw models list 解析到模型。请先运行 openclaw onboard 并确认模型可用。',
'dash.noticeTitle':'📌 Telegram 使用说明(重要)',
'dash.notice1':'OCM 主要面向 Telegram:通过群组绑定 Agent,让每个 Agent 拥有独立 Workspace / SOUL.md / MEMORY.md。',
'dash.notice2':'请确保你已有基本 OpenClaw 操作经验。OCM 主要负责可视化更新 openclaw.json,方便增删主 Agent 与 Sub-Agent,并支持多条 Agent 树。',
'dash.notice3':'BotFather 中请确认 Allow Groups = ON 且 Group Privacy = OFF,否则 Sub-Agent 可能无法入组或无法响应。',
'dash.noticeWarn':'⚠️ 强烈建议:每个 Agent 群组只保留你自己和该 Agent(或其 Sub-Agent)。不要邀请其他人,把每个组当作私聊空间。',
'auth.title':'认证配置','auth.configured':'已配置认证',
'auth.guide':'点击 Provider 查看认证步骤。认证需在终端完成,或使用下方 CLI 终端。',
'auth.step1':'1. 获取 API Key','auth.step2':'2. 在终端运行以下命令','auth.step3':'3. 按提示粘贴 API Key 并回车',
@@ -2144,9 +2293,13 @@ const I18N = {
'guide.agent.s2':'Send <code>/newbot</code> and follow prompts to name your Bot',
'guide.agent.s3':'Copy the <code>Bot Token</code> from BotFather and paste below',
'guide.agent.s4':'Send <code>/setprivacy</code> → select your Bot → click <code>Disable</code> (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 <code>peer.id</code> (negative number) in gateway logs',
'guide.sub.s3':'Fill in the Group ID and Agent config below',
'guide.sub.s1':'在 BotFather 发送 <code>/newbot</code> 创建新 Bot,获取 Bot Token',
'guide.sub.s2':'在 BotFather 发送 <code>/mybots</code> → 选择 Bot → <b>Bot Settings</b> → <b>Group Privacy</b> → <b>Turn off</b>',
'guide.sub.s3':'在 Telegram 创建新群组,将 Bot 加入群组(<b>不要加其他人</b>',
'guide.sub.s4':'在群内发一条消息,从 gateway 日志中找到 <code>peer.id</code>(负数)',
'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':'默认',
@@ -2213,6 +2366,14 @@ const I18N = {
'channels.title':'Channel Bindings','channels.add':' Add Binding','channels.hint':'Manage Agent to channel/group bindings. Order determines priority.',
'models.title':'Model Management','models.primary':'Default Primary Model','models.fallback':'Fallback Chain',
'models.hint':'Models are registered via openclaw onboard. Manage primary model and fallback chain here.',
'models.onlyCliHint':'Model dropdowns only show IDs returned by openclaw models list.',
'models.modelListErr':'Failed to load openclaw models list: ',
'models.modelListEmpty':'No model IDs were parsed from openclaw models list. Run openclaw onboard first.',
'dash.noticeTitle':'📌 Telegram Usage Notes (Important)',
'dash.notice1':'OCM is primarily for Telegram workflows: bind agents to groups so each agent has isolated Workspace / SOUL.md / MEMORY.md.',
'dash.notice2':'Basic OpenClaw CLI experience is required. OCM focuses on visual openclaw.json updates for easier main-agent/sub-agent management and multiple agent trees.',
'dash.notice3':'In BotFather, keep Allow Groups = ON and Group Privacy = OFF; otherwise sub-agents may fail to join or respond.',
'dash.noticeWarn':'⚠️ Strong recommendation: each agent group should include only you and that agent (or its sub-agents). Treat each group like a private chat.',
'auth.title':'Auth Config','auth.configured':'Configured Auth',
'auth.guide':'Click a Provider for setup instructions. Auth is done in terminal or via the CLI panel below.',
'auth.step1':'1. Get API Key','auth.step2':'2. Run the command below in terminal','auth.step3':'3. Paste your API Key when prompted',
@@ -2251,9 +2412,13 @@ const I18N = {
'guide.agent.s2':'Send <code>/newbot</code> and follow prompts to name your Bot',
'guide.agent.s3':'Copy the <code>Bot Token</code> from BotFather and paste below',
'guide.agent.s4':'Send <code>/setprivacy</code> → select your Bot → click <code>Disable</code> (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 <code>peer.id</code> (negative number) in gateway logs',
'guide.sub.s3':'Fill in the Group ID and Agent config below',
'guide.sub.s1':'Send <code>/newbot</code> to BotFather to create a new Bot and get the Bot Token',
'guide.sub.s2':'Send <code>/mybots</code> to BotFather → select your Bot → <b>Bot Settings</b> → <b>Group Privacy</b> → <b>Turn off</b>',
'guide.sub.s3':'Create a new Telegram group, add the Bot to the group (<b>do NOT add anyone else</b>)',
'guide.sub.s4':'Send a message in the group, find <code>peer.id</code> (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',
@@ -2371,7 +2536,7 @@ const LANDING_TEXT = {
},
};
// ── 全局状态 ────────────────────────────────────────────────
let S = { agents:[], channels:[], models:{}, authProfiles:{}, knownModels:[], authProviders:[], primaryModel:'', fallbacks:[] };
let S = { agents:[], channels:[], models:{}, authProfiles:{}, knownModels:[], modelListError:'', authProviders:[], primaryModel:'', fallbacks:[] };
let wizCur = 1;
let logTimer = null;
let selectedAuthProv = null;
@@ -2391,24 +2556,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<u.length-1){b/=1024;i++;}return b.toFixed(i>0?1:0)+' '+u[i];}
function fmtUptime(s){const d=Math.floor(s/86400);const h=Math.floor((s%86400)/3600);const m=Math.floor((s%3600)/60);if(d>0)return d+'d '+h+'h '+m+'m';if(h>0)return h+'h '+m+'m';return m+'m';}
function dashRow(label,val){return '<div class="dash-row"><span class="dash-label">'+esc(label)+'</span><span class="dash-val">'+val+'</span></div>';}
function dashBar(pct){const cls=pct>90?'danger':pct>70?'warn':'';return '<div class="dash-bar-wrap"><div class="dash-bar '+cls+'" style="width:'+Math.min(pct,100)+'%"></div></div>';}
function gaugeColor(pct){if(pct>90)return '#ef4444';if(pct>70)return '#f59e0b';return '#22c55e';}
function buildGaugeSVG(pct,label,sub,color){
const r=46,cx=55,cy=55,sw=8;
const circ=2*Math.PI*r;
const gap=circ*0.25;
const arc=circ-gap;
const filled=arc*(Math.min(pct,100)/100);
const rot=135;
if(!color)color=gaugeColor(pct);
return '<div class="dash-gauge-card">'+
'<svg class="dash-gauge-svg" viewBox="0 0 110 110">'+
'<circle cx="'+cx+'" cy="'+cy+'" r="'+r+'" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="'+sw+'" stroke-dasharray="'+arc+' '+gap+'" stroke-linecap="round" transform="rotate('+rot+' '+cx+' '+cy+')"/>'+
'<circle cx="'+cx+'" cy="'+cy+'" r="'+r+'" fill="none" stroke="'+color+'" stroke-width="'+sw+'" stroke-dasharray="'+filled+' '+(circ-filled)+'" stroke-linecap="round" transform="rotate('+rot+' '+cx+' '+cy+')" style="transition:stroke-dasharray .6s ease"/>'+
'<text x="'+cx+'" y="'+cy+'" text-anchor="middle" dy="-2" fill="'+color+'" font-size="22" font-weight="700" font-family="-apple-system,sans-serif">'+Math.round(pct)+'%</text>'+
'<text x="'+cx+'" y="'+(cy+14)+'" text-anchor="middle" fill="rgba(255,255,255,0.5)" font-size="10" font-family="-apple-system,sans-serif">'+esc(sub)+'</text>'+
'</svg>'+
'<div class="dash-gauge-label">'+esc(label)+'</div></div>';
}
function toggleDashRefresh(on){
if(dashRefreshTimer){clearInterval(dashRefreshTimer);dashRefreshTimer=null;}
if(on){dashRefreshTimer=setInterval(loadDashboard,10000);}
}
async function loadDashboard(){
try{
const r=await api('GET','/api/dashboard');
if(!r.ok)return;
const s=r.system, g=r.gateway, a=r.agents;
// System card
const memPct=s.totalMem?((s.totalMem-s.freeMem)/s.totalMem*100):0;
const diskPct=s.diskTotal?(s.diskUsed/s.diskTotal*100):0;
const cpuPct=(s.cpuPercent!==null&&s.cpuPercent!==undefined)?s.cpuPercent:0;
// Gauges
const memUsed=fmtBytes(s.totalMem-s.freeMem)+' / '+fmtBytes(s.totalMem);
const diskUsed=fmtBytes(s.diskUsed)+' / '+fmtBytes(s.diskTotal);
let gaugeHtml=buildGaugeSVG(cpuPct,'CPU',s.cpuCores+' cores');
gaugeHtml+=buildGaugeSVG(memPct,'RAM',memUsed);
gaugeHtml+=buildGaugeSVG(diskPct,'DISK',diskUsed);
document.getElementById('dashGauges').innerHTML=gaugeHtml;
// System info card
const loadStr=s.loadAvg?s.loadAvg.map(function(v){return v.toFixed(2);}).join(' / '):'—';
let sysHtml=dashRow('Hostname',esc(s.hostname));
sysHtml+=dashRow('OS',esc(s.platform));
sysHtml+=dashRow('Node.js',esc(s.nodeVer));
sysHtml+=dashRow('CPU',esc(s.cpuModel)+' ('+s.cpuCores+' cores)');
sysHtml+=dashRow('CPU',esc(s.cpuModel));
sysHtml+=dashRow('Cores',String(s.cpuCores));
sysHtml+=dashRow('Uptime',fmtUptime(s.uptime));
sysHtml+=dashRow('Memory',fmtBytes(s.totalMem-s.freeMem)+' / '+fmtBytes(s.totalMem)+' ('+memPct.toFixed(0)+'%)');
sysHtml+=dashBar(memPct);
sysHtml+=dashRow('Load Avg (1/5/15m)',loadStr);
document.getElementById('dashSysItems').innerHTML=sysHtml;
// Gateway card
const statusIcon='<span class="dash-indicator '+esc(g.status)+'"></span>';
@@ -2419,7 +2616,9 @@ async function loadDashboard(){
gwHtml+=dashRow('HTTP Ping',g.ping?'<span style="color:#22c55e">✓ Reachable</span>':'<span style="color:#ef4444">✗ Unreachable</span>');
document.getElementById('dashGwItems').innerHTML=gwHtml;
// Agents card
let agHtml=dashRow('Total Agents',String(a.count));
let agHtml=dashRow('Main Agents',String(a.mainCount||0));
agHtml+=dashRow('Sub-Agents',String(a.subCount||0));
agHtml+=dashRow('Total',String(a.count));
if(a.lastActivity){
const d=new Date(a.lastActivity);
agHtml+=dashRow('Last Activity',d.toLocaleString('en-AU',{timeZone:'Australia/Brisbane',hour12:false}));
@@ -2430,13 +2629,8 @@ async function loadDashboard(){
agHtml+=dashRow('Server Time',esc(r.serverTime));
document.getElementById('dashAgentItems').innerHTML=agHtml;
// Storage card
let stHtml=dashRow('OpenClaw Dir Size',fmtBytes(s.dirSize));
if(s.diskTotal>0){
const diskPct=s.diskUsed/s.diskTotal*100;
stHtml+=dashRow('Disk',fmtBytes(s.diskUsed)+' / '+fmtBytes(s.diskTotal)+' ('+diskPct.toFixed(0)+'%)');
stHtml+=dashBar(diskPct);
stHtml+=dashRow('Disk Free',fmtBytes(s.diskFree));
}
let stHtml=dashRow('OpenClaw Dir',fmtBytes(s.dirSize));
stHtml+=dashRow('Disk Free',fmtBytes(s.diskFree));
document.getElementById('dashStorageItems').innerHTML=stHtml;
dashLoaded=true;
}catch(e){console.error('Dashboard load error:',e);}
@@ -2480,6 +2674,7 @@ async function loadModels(){
const r=await api('GET','/api/models');
S.models=r.models||{}; S.authProfiles=r.authProfiles||{};
S.knownModels=r.knownModels||[]; S.authProviders=r.authProviders||[];
S.modelListError=r.modelListError||'';
S.primaryModel=r.primaryModel||''; S.fallbacks=r.fallbacks||[];
renderModels(); renderAuth(); buildModelDropdowns();
}catch(e){ toast('加载模型失败: '+e.message,'error'); }
@@ -2528,6 +2723,11 @@ function buildAddAgentForm() {
'<div class="form-group"><label>' + t('wiz.model') + '</label>' +
'<select id="fa-model">' + modelOpts + '</select>' +
'</div>' +
'<div class="form-group"><label>' + t('wiz.purpose') + '</label>' +
'<textarea id="fa-purpose" placeholder="' + t('wiz.purposePh') + '" rows="2"></textarea></div>' +
'<div class="form-group"><label>' + t('wiz.soul') + ' <span style="color:var(--muted);font-weight:400">' + t('wiz.soulHint') + '</span></label>' +
'<input id="fa-soul" placeholder="' + t('wiz.soulPh') + '">' +
'<span class="hint-text">' + t('wiz.soulTip') + '</span></div>' +
'<div style="display:flex;gap:8px;margin-top:14px">' +
'<button class="btn-primary" onclick="submitAddAgent()">' + t('agents.addAgentSubmit') + '</button>' +
'<button class="btn-ghost" onclick="clearAddForm()">' + t('btn.cancel') + '</button>' +
@@ -2553,12 +2753,19 @@ function buildAddSubForm() {
'<li>' + t('guide.sub.s1') + '</li>' +
'<li>' + t('guide.sub.s2') + '</li>' +
'<li>' + t('guide.sub.s3') + '</li>' +
'</ol></details>' +
'<li>' + t('guide.sub.s4') + '</li>' +
'<li>' + t('guide.sub.s5') + '</li>' +
'</ol>' +
'<div style="margin-top:10px;padding:10px 12px;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:8px;font-size:13px;line-height:1.5">' + t('guide.sub.warn') + '</div>' +
'</details>' +
'<div class="form-group"><label>Parent Agent</label>' +
'<select id="fs-parent">' + parentOpts + '</select></div>' +
'<div class="form-group"><label>' + t('wiz.groupId') + '</label>' +
'<input id="fs-gid" placeholder="-100XXXXXXXXXX">' +
'<span class="hint-text">' + t('wiz.groupHint') + '</span></div>' +
'<div class="form-group"><label>' + t('wiz.telegramId') + '</label>' +
'<input id="fs-tgid" placeholder="' + t('wiz.telegramIdPh') + '">' +
'<span class="hint-text">' + t('wiz.telegramIdHint') + '</span></div>' +
'<div class="form-row">' +
'<div class="form-group"><label>' + t('wiz.agentId') + ' <span style="color:var(--muted);font-weight:400">' + t('wiz.agentIdHint') + '</span></label>' +
'<input id="fs-aid" placeholder="' + t('wiz.agentIdPh') + '"></div>' +
@@ -2585,13 +2792,15 @@ async function submitAddAgent() {
const name = document.getElementById('fa-name').value.trim();
const workspace = document.getElementById('fa-workspace').value.trim();
const model = document.getElementById('fa-model').value;
const purpose = (document.getElementById('fa-purpose')?.value||'').trim();
const personality = (document.getElementById('fa-soul')?.value||'').trim();
if (!token) { toast(t('agents.errToken'), 'err'); return; }
if (!agentId) { toast('Agent ID is required', 'err'); return; }
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) { toast('Agent ID must contain only alphanumeric characters, underscores, or dashes', 'err'); return; }
if (!name) { toast(t('agents.errName'), 'err'); return; }
if (!workspace) { toast('Workspace name is required', 'err'); return; }
try {
const payload = { botToken: token, agentId, name, workspace, model: model === '__default__' ? '' : model };
const payload = { botToken: token, agentId, name, workspace, model: model === '__default__' ? '' : model, purpose, personality };
const r = await api('POST', '/api/agents/bot', payload);
toast('Agent created successfully', 'ok');
closePopover();
@@ -2606,6 +2815,7 @@ async function submitAddAgent() {
async function submitAddSub() {
const parentAgentId = document.getElementById('fs-parent').value.trim();
const groupId = document.getElementById('fs-gid').value.trim();
const telegramUserId = (document.getElementById('fs-tgid')?.value||'').trim();
const agentId = document.getElementById('fs-aid').value.trim();
const displayName = document.getElementById('fs-name').value.trim();
const model = document.getElementById('fs-model').value;
@@ -2616,8 +2826,9 @@ async function submitAddSub() {
if (!groupId) { toast(t('wiz.errGroupId'), 'err'); return; }
if (!agentId) { toast(t('wiz.errAgentId'), 'err'); return; }
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) { toast(t('wiz.errIdFormat'), 'err'); return; }
if (telegramUserId && !/^\\d+$/.test(telegramUserId)) { toast('Telegram User ID must be a number', 'err'); return; }
try {
const r = await api('POST', '/api/agents', { parentAgentId, agentId, displayName: displayName || agentId, groupId, workspaceFolder: agentId, model: model === '__default__' ? '' : model, purpose, personality, initialMemory });
const r = await api('POST', '/api/agents', { parentAgentId, agentId, displayName: displayName || agentId, groupId, workspaceFolder: agentId, model: model === '__default__' ? '' : model, purpose, personality, initialMemory, telegramUserId });
if (r.error) { toast(r.error, 'err'); return; }
toast(t('wiz.created'), 'ok');
document.getElementById('addFormArea').innerHTML = '';
@@ -2631,63 +2842,93 @@ function renderAgents() {
const el = document.getElementById('agentTree');
if (!S.agents.length) { el.innerHTML = '<div class="empty">' + t('agents.empty') + '</div>'; return; }
// Build tree: main agent as root, subagents as children
const mainAgent = S.agents.find(a => a.id === 'main');
const subs = S.agents.filter(a => a.id !== 'main');
// Build tree: each hasOwnBot agent is a root, others are sub-agents grouped by parentAccountId
const roots = S.agents.filter(a => a.hasOwnBot);
const subs = S.agents.filter(a => !a.hasOwnBot);
let html = '';
// Map accountId -> root agent for sub-agent grouping
const rootByAccount = {};
roots.forEach(a => { if (a.accountId) rootByAccount[a.accountId] = a; });
// Main agent tree
if (mainAgent) {
html += '<div class="agent-tree-root">';
html += '<div class="tree-main">';
html += '<div class="tree-title">🤖 ' + esc(mainAgent.name || 'main') + ' <span class="badge main">' + t('agents.main') + '</span></div>';
html += '<div class="tree-meta">🧠 ' + esc(mainAgent.effectiveModel) + '</div>';
if (mainAgent.workspace) html += '<div class="tree-meta">📁 ' + esc(mainAgent.workspace) + '</div>';
html += '<div class="tree-actions">';
html += '<select class="inline-sel" id="msel-main" onchange="">' + buildModelOpts(mainAgent.effectiveModel !== t('agents.noModel') ? mainAgent.effectiveModel : '__default__') + '</select>';
html += '<button class="btn-secondary" onclick="saveAgentModel(\\'main\\')">' + t('agents.saveModel') + '</button>';
html += '</div></div>';
// Sub-agents as children
if (subs.length) {
html += '<div class="tree-children">';
subs.forEach(a => {
html += '<div class="tree-child">';
html += '<div class="tree-title">📱 ' + esc(a.name || a.id);
if (a.groupId) html += ' <span class="badge ok">' + esc(a.groupId) + '</span>';
html += '</div>';
html += '<div class="tree-meta">🧠 ' + esc(a.effectiveModel) + '</div>';
if (a.workspace) html += '<div class="tree-meta">📁 ' + esc(a.workspace) + '</div>';
html += '<div class="tree-actions">';
html += '<select class="inline-sel" id="msel-' + a.id + '" onchange="">' + buildModelOpts(a.effectiveModel !== t('agents.noModel') ? a.effectiveModel : '__default__') + '</select>';
html += '<button class="btn-secondary" onclick="saveAgentModel(\\'' + a.id + '\\')">' + t('agents.saveModel') + '</button>';
html += '<button class="btn-secondary" onclick="viewWorkspace(\\'' + a.id + '\\')">' + t('agents.viewFiles') + '</button>';
html += '<button class="btn-danger" onclick="deleteAgent(\\'' + a.id + '\\',\\'' + esc(a.name || a.id) + '\\')">' + t('btn.delete') + '</button>';
html += '</div></div>';
});
html += '</div>';
// Group subs under their parent
const subsByRoot = {};
const orphanSubs = [];
subs.forEach(a => {
const parentAcct = a.parentAccountId;
if (parentAcct && rootByAccount[parentAcct]) {
const rootId = rootByAccount[parentAcct].id;
if (!subsByRoot[rootId]) subsByRoot[rootId] = [];
subsByRoot[rootId].push(a);
} else {
orphanSubs.push(a);
}
html += '</div>';
} else if (subs.length) {
// No main agent but has subs (edge case)
subs.forEach(a => {
html += '<div class="agent-tree-root"><div class="tree-child">';
html += '<div class="tree-title">📱 ' + esc(a.name || a.id);
if (a.groupId) html += ' <span class="badge ok">' + esc(a.groupId) + '</span>';
html += '</div>';
html += '<div class="tree-meta">🧠 ' + esc(a.effectiveModel) + '</div>';
if (a.workspace) html += '<div class="tree-meta">📁 ' + esc(a.workspace) + '</div>';
html += '<div class="tree-actions">';
html += '<select class="inline-sel" id="msel-' + a.id + '" onchange="">' + buildModelOpts(a.effectiveModel !== t('agents.noModel') ? a.effectiveModel : '__default__') + '</select>';
html += '<button class="btn-secondary" onclick="saveAgentModel(\\'' + a.id + '\\')">' + t('agents.saveModel') + '</button>';
html += '<button class="btn-secondary" onclick="viewWorkspace(\\'' + a.id + '\\')">' + t('agents.viewFiles') + '</button>';
html += '<button class="btn-danger" onclick="deleteAgent(\\'' + a.id + '\\',\\'' + esc(a.name || a.id) + '\\')">' + t('btn.delete') + '</button>';
html += '</div></div></div>';
});
});
function agentCard(a, isRoot) {
let h = '';
const icon = isRoot ? '🤖' : '📱';
const cls = isRoot ? 'tree-main' : 'tree-child';
h += '<div class="' + cls + '">';
h += '<div class="tree-title">' + icon + ' ' + esc(a.name || a.id);
if (isRoot) h += ' <span class="badge main">' + (a.id === 'main' ? t('agents.main') : 'Bot') + '</span>';
if (a.groupId) h += ' <span class="badge ok">' + esc(a.groupId) + '</span>';
h += '</div>';
h += '<div class="tree-meta">🧠 ' + esc(a.effectiveModel) + '</div>';
if (a.workspace) h += '<div class="tree-meta">📁 ' + esc(a.workspace) + '</div>';
h += '<div class="tree-actions">';
h += '<select class="inline-sel" id="msel-' + a.id + '">' + buildModelOpts(a.effectiveModel !== t('agents.noModel') ? a.effectiveModel : '__default__') + '</select>';
h += '<button class="btn-secondary" data-action="saveModel" data-id="' + esc(a.id) + '">' + t('agents.saveModel') + '</button>';
h += '<button class="btn-secondary" data-action="viewWs" data-id="' + esc(a.id) + '">' + t('agents.viewFiles') + '</button>';
if (a.id !== 'main') h += '<button class="btn-danger" data-action="delAgent" data-id="' + esc(a.id) + '" data-name="' + esc(a.name || a.id) + '">' + t('btn.delete') + '</button>';
h += '</div></div>';
return h;
}
let html = '<div class="agents-roots">';
// Render each root with its children, side by side
roots.forEach(root => {
const children = subsByRoot[root.id] || [];
html += '<div class="agent-tree-root">';
html += agentCard(root, true);
if (children.length) {
const treeId = 'tree-' + root.id;
html += '<div class="tree-children-wrap">';
html += '<button class="tree-toggle" onclick="toggleTree(\\'' + treeId + '\\',this)" title="Expand / Collapse"></button>';
html += '<div class="tree-children" id="' + treeId + '">';
children.forEach(c => { html += agentCard(c, false); });
html += '</div></div>';
}
html += '</div>';
});
// Orphan subs (no matching root — edge case)
orphanSubs.forEach(a => {
html += '<div class="agent-tree-root">';
html += agentCard(a, false);
html += '</div>';
});
html += '</div>';
el.innerHTML = html;
// Event delegation for agent tree buttons
el.onclick = function(ev) {
const btn = ev.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
const id = btn.dataset.id;
if (action === 'saveModel') saveAgentModel(id);
else if (action === 'viewWs') viewWorkspace(id);
else if (action === 'delAgent') deleteAgent(id, btn.dataset.name);
};
}
function toggleTree(treeId, btn) {
const el = document.getElementById(treeId);
if (!el) return;
el.classList.toggle('collapsed');
btn.textContent = el.classList.contains('collapsed') ? '+' : '';
}
function buildModelOpts(selected){
@@ -2702,10 +2943,6 @@ function buildModelOpts(selected){
opts+=\`<option value="\${m.id}"\${selected===m.id?' selected':''}>\${m.label}</option>\`;
});
if(lastGroup) opts+=\`</optgroup>\`;
Object.keys(S.models).forEach(id=>{
if(!S.knownModels.find(k=>k.id===id))
opts+=\`<option value="\${id}"\${selected===id?' selected':''}>\${id} (\${t('agents.custom')})</option>\`;
});
return opts;
}
@@ -2771,6 +3008,19 @@ async function deleteChannel(idx,label){
// ── 渲染模型 ─────────────────────────────────────────────────
function renderModels(){
const listWarn=document.getElementById('modelListWarn');
if(listWarn){
if(S.modelListError){
listWarn.style.display='';
listWarn.textContent=t('models.modelListErr')+S.modelListError;
}else if(!S.knownModels.length){
listWarn.style.display='';
listWarn.textContent=t('models.modelListEmpty');
}else{
listWarn.style.display='none';
listWarn.textContent='';
}
}
// 检测 primary model 是否是 API Key(显示修复警告)
const primWarn=document.getElementById('primaryModelWarn');
if(primWarn){
@@ -2787,7 +3037,7 @@ function renderModels(){
}
}
const pSel=document.getElementById('primaryModelSel');
pSel.innerHTML='';
pSel.innerHTML='<option value="">'+(lang==='en'?'(no models loaded)':'(未加载到模型)')+'</option>';
S.knownModels.filter(m=>m.id!=='__default__').forEach(m=>{
pSel.innerHTML+=\`<option value="\${m.id}"\${S.primaryModel===m.id?' selected':''}>\${m.label}</option>\`;
});
@@ -2828,7 +3078,7 @@ async function savePrimaryModel(){
// 修复被错误设置为 API Key 的主模型 → 重置为第一个已注册模型或留空
async function fixBadPrimaryModel(){
// 尝试从已注册模型中取第一个可用 ID
const firstModel = Object.keys(S.models||{}).find(k => isValidModelId(k));
const firstModel = (S.knownModels||[]).map(m=>m.id).find(k => isValidModelId(k));
const resetTo = firstModel || '';
if(!confirm(\`将主模型重置为"\${resetTo||'(清空,使用全局默认)'}"\`)) return;
try{
@@ -2982,7 +3232,7 @@ async function refreshAuthOnly(){
try{
const r=await api('GET','/api/models');
S.authProfiles=r.authProfiles||{};
S.models=r.models||{}; S.knownModels=r.knownModels||[];
S.models=r.models||{}; S.knownModels=r.knownModels||[]; S.modelListError=r.modelListError||'';
renderAuth(); buildModelDropdowns();
}catch(e){ toast((lang==='en'?'Refresh failed: ':'刷新失败: ')+e.message,'error'); }
}
+2 -2
View File
@@ -1,6 +1,6 @@
@echo off
:: ================================================================
:: OpenClaw Manager v0.6.0 — Start Script (Windows)
:: OpenClaw Manager v0.7.1 — Start Script (Windows)
::
:: Features:
:: * Auto-detect Node.js, show install instructions if missing
@@ -19,7 +19,7 @@ set "MIN_NODE_MAJOR=18"
echo.
echo ===================================
echo OpenClaw Manager v0.6.0
echo OpenClaw Manager v0.7.1
echo -----------------------------------
echo.
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# ================================================================
# OpenClaw Manager v0.6.0 — Start Script (macOS / Linux)
# OpenClaw Manager v0.7.1 — Start Script (macOS / Linux)
#
# Features:
# - Auto-detect Node.js, show install instructions if missing
@@ -22,7 +22,7 @@ CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
banner() {
echo ""
echo -e "${CYAN}${BOLD} 🦀 OpenClaw Manager v0.6.0${RESET}"
echo -e "${CYAN}${BOLD} 🦀 OpenClaw Manager v0.7.1${RESET}"
echo -e "${DIM} ─────────────────────────────${RESET}"
echo ""
}