21 Commits
Author SHA1 Message Date
taodeng 428e92baa6 docs: remove arrow annotations; strengthen screenshot redactions 2026-02-28 15:53:38 +10:00
taodeng e08c782559 docs: add FAQ/security/changelog 2026-02-28 15:42:38 +10:00
taodeng 8e9b07a01e docs: add annotated screenshots with arrows (README + guides) 2026-02-28 14:46:52 +10:00
taodeng 8c20735ff5 docs: add built-in CLI feature (screenshots + guides) 2026-02-28 14:44:53 +10:00
taodeng 25d8b94209 docs: link English usage guide 2026-02-28 14:25:52 +10:00
taodeng 45e63f7440 docs: add English usage guide (with screenshots) 2026-02-28 14:25:45 +10:00
taodeng bd5c3f32ca docs: link zh-CN usage guide 2026-02-28 14:23:50 +10:00
taodeng f1bdf702ad docs: add detailed zh-CN usage guide 2026-02-28 14:23:38 +10:00
taodeng 78cb04931d docs: update README for v0.7.x features 2026-02-28 14:10:12 +10:00
taodeng 8a4169ac83 docs: embed redacted screenshots in README 2026-02-28 14:06:55 +10:00
taodeng 4fd70438b8 docs: link to redacted screenshots 2026-02-28 13:22:49 +10:00
taodeng 1680834ab2 docs: add redacted OCM screenshots 2026-02-28 13:21:51 +10:00
taodeng b0c707b74a chore: sync release metadata to v0.7.1 and add telegram safety notes 2026-02-28 07:37:53 +10:00
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
20 changed files with 1210 additions and 147 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 中文 → 逐步迁移为英文
---
+2 -2
View File
@@ -9,9 +9,9 @@
<key>CFBundleIdentifier</key>
<string>com.openclaw.manager</string>
<key>CFBundleVersion</key>
<string>0.5.0</string>
<string>0.7.1</string>
<key>CFBundleShortVersionString</key>
<string>0.5.0</string>
<string>0.7.1</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleIconFile</key>
+71 -2
View File
@@ -2,7 +2,7 @@
A zero-dependency, single-file web dashboard for [OpenClaw](https://github.com/anthropics/openclaw). Manage agents, monitor token usage, and run commands — all from your browser, no `npm install` required.
[中文说明](#中文说明)
[中文说明](#中文说明) · [中文使用说明(含截图)](docs/USAGE_GUIDE.zh-CN.md) · [English Guide (with screenshots)](docs/USAGE_GUIDE.en.md)
---
@@ -22,21 +22,74 @@ git pull --ff-only
```
Open [http://localhost:3333](http://localhost:3333) in your browser. For remote access, use `bash start.sh --host 0.0.0.0`.
## Docs
- English guide: docs/USAGE_GUIDE.en.md
- 中文使用说明: docs/USAGE_GUIDE.zh-CN.md
- FAQ: docs/FAQ.md
- Security notes: docs/SECURITY.md
- Changelog: docs/CHANGELOG.md
## Features
**Screenshots** — Redacted UI walkthrough (no personal paths, no Telegram IDs):
![](docs/redacted-screenshots/dashboard.jpg)
![](docs/redacted-screenshots/agents.jpg)
![](docs/redacted-screenshots/channels.jpg)
![](docs/redacted-screenshots/models.jpg)
![](docs/redacted-screenshots/auth.jpg)
![](docs/redacted-screenshots/stats.jpg)
![](docs/redacted-screenshots/cron.jpg)
**Agent Management** — Add main agents and sub-agents through a guided setup flow. View all agents in a tree structure with model selection, workspace browsing, and inline configuration.
**Usage Statistics** — Real token usage data parsed directly from OpenClaw session files. Breakdown by model, agent, and day with a visual chart.
**Model & Auth** — Configure models from your registered provider list. Follow built-in guides for provider authentication setup.
**Built-in CLI** — Run any `openclaw` command with real-time streaming output. Preset commands, favorites, and tab completion included.
**Built-in CLI** — Run any OpenClaw CLI command with real-time streaming output. Includes tab completion and saved favorites.
![](docs/redacted-screenshots/cli.jpg)
**Ops Panel** — Restart gateway, view logs, run health checks, manage backups (local + NAS via SFTP/rsync), and handle cron tasks.
**Bilingual UI** — English and Chinese interface with one-click language switching.
## What's New (v0.7.x)
- **Dashboard redesign**: circular CPU/RAM/DISK gauges + system/gateway/agent overview, with optional auto-refresh.
- **Sub-agent creation flow**: step-by-step Telegram guide (BotFather → group → get group ID → fill form), with explicit safety warnings.
- **Telegram allowlist helper**: optional “Your Telegram User ID” field can auto-append to `channels.telegram.allowFrom`.
- **Model dropdown = real CLI output**: selectors are sourced from `openclaw models list` (with a warning if parsing fails).
- **Cache-busting**: response headers + version checks help prevent stale frontend after updates.
- **Setup page EN localization**: first-run directory selection page is fully bilingual.
## Telegram Workflow & Safety
OCM is designed primarily for **Telegram-based OpenClaw workflows**:
- Bind Telegram groups to one or more main agents, then create sub-agents under each tree
- Keep each agent's context isolated via independent `workspace`, `SOUL.md`, and `MEMORY.md`
- OCM mainly helps you **safely update `openclaw.json`** via UI instead of manual editing
- Recommended for users who already have basic OpenClaw CLI experience (`onboard`, auth, gateway logs)
Critical Telegram settings:
- In BotFather, keep **Allow Groups = ON**
- In BotFather, set **Group Privacy = OFF**
- For each agent group, keep it private: **only you + that agent/sub-agents**
- Do **not** invite other people to these groups (cost and security risk)
## Requirements
- Node.js 18+ ([download](https://nodejs.org/))
@@ -131,6 +184,22 @@ start.bat # Windows
- **运维面板** — 重启网关、查看日志、健康检查、本地/NAS 备份、Cron 任务管理
- **双语界面** — 中英文一键切换
### Telegram 场景与安全提示
OCM 主要面向 **Telegram 场景**
- 通过群组绑定主 Agent,并在每条树下管理多个 Sub-Agent
- 让每个 Agent 拥有独立 `workspace``SOUL.md``MEMORY.md`
- 通过可视化方式更新 `openclaw.json`,减少手动改配置风险
- 建议使用者已具备基础 OpenClaw CLI 操作经验(如 `onboard`、认证、查看网关日志)
关键安全设置(务必确认):
- BotFather 里 **Allow Groups = ON**
- BotFather 里 **Group Privacy = OFF**
- 每个 Agent 群组只保留“你自己 + 对应 Agent/Sub-Agent”
- 不要邀请其他人进组(会带来安全和 API 费用风险)
### 配置
启动脚本自动检测 `~/.openclaw` 目录。如需指定其他路径:
+26
View File
@@ -0,0 +1,26 @@
# Changelog
This is a user-facing summary derived from `DEVLOG.md`.
## v0.7.6
- Docs: annotated screenshots with arrows for key UI controls.
## v0.7.5
- Docs: built-in CLI terminal instructions + screenshot (tab completion, favorites, streaming output).
## v0.7.4
- Docs: English + Chinese usage guides with redacted screenshots.
## v0.7.1
- Dashboard guidance block for Telegram-first workflow + safety checklist.
- Model dropdown options sourced from `openclaw models list`.
## v0.7.0
- Dashboard redesign with circular CPU/RAM/DISK gauges and auto-refresh.
- Sub-agent creation flow rewritten (BotFather → group → group id → form) including allowlist helper.
## v0.6.6
- Fix: sub-agents correctly grouped under main for older config formats.
## v0.6.5
- New Dashboard tab + cache-busting headers and version checks.
+53
View File
@@ -0,0 +1,53 @@
# OCM FAQ
## Q: Telegram 群里经常显示 typing,但不回消息?
Most common cause is **BotFather Group Privacy** being ON.
Fix:
- BotFather → Your bot → Bot Settings → **Group Privacy****Turn off**
Also check:
- `openclaw status` (gateway healthy?)
- provider auth/rate limits (OCM → Auth page, and gateway logs)
## Q: 如何获取 Telegram Group IDPeer ID / -100...)?
Typical workflow:
```bash
openclaw gateway logs --follow
```
Send a message in the target group; the gateway logs usually contain the peer id.
## Q: 模型下拉列表为什么没有我想要的模型?
OCM sources model options from the real CLI output:
```bash
openclaw models list
```
So if it doesn't show up in OCM, make sure it's registered/available in your OpenClaw environment.
## Q: Dashboard 里 Gateway HTTP Ping 显示 Unreachable
Common reasons:
- Gateway not running
- Gateway binds to a different interface/port than OCM expects
- Local firewall/proxy interference
Try:
- `openclaw status`
- `openclaw gateway restart`
## Q: OCM 会把我的 token/个人信息上传出去吗?
OCM is designed as a **local** dashboard.
That said, your local OpenClaw config contains sensitive information. Follow the safety guidance:
- keep agent Telegram groups private
- don't share raw screenshots without redaction
See: `docs/SECURITY.md`.
+30
View File
@@ -0,0 +1,30 @@
# Security Notes
OCM is a **local web UI** for managing OpenClaw. Security depends heavily on how you run OpenClaw and how you use Telegram.
## Recommended defaults
- **Run locally** and restrict host binding unless you intentionally expose it.
- Treat OpenClaw config as secrets: tokens, provider keys, account ids.
- Prefer separate workspaces for each agent/sub-agent.
## Telegram safety
Strongly recommended:
- **One Telegram group = one agent boundary** (context + purpose + workspace)
- Keep each agent group **private**: only you + the bot (and optionally your second account)
- Do **not** invite other people (cost + security risk)
Critical BotFather settings:
- Allow Groups = ON
- Group Privacy = OFF
## Sharing screenshots
If you share screenshots publicly:
- blur personal paths (e.g. `/Users/<name>/...`)
- blur Telegram group/peer IDs (e.g. `-100xxxxxxxxxx`)
- blur any tokens/keys if visible
This repo contains **redacted + annotated** screenshots under `docs/annotated-screenshots/`.
+275
View File
@@ -0,0 +1,275 @@
# OCM Usage Guide (with screenshots)
> Audience: People who already have OpenClaw running locally (you can run `openclaw status` and read `openclaw gateway logs`).
>
> Goal: Use **OpenClaw Manager (OCM)** to make agent/sub-agent setup and day-to-day ops **visual, safer, and easier to audit**.
---
## 0) The pain: why OCM exists
Creating and maintaining OpenClaw agents is powerful, but the “default workflow” is often messy:
1) **Editing `openclaw.json` is risky**
- Deep/nested config, many fields.
- One accidental overwrite can break multiple agents (tokens, bindings, accountIds).
2) **Sub-agent topology is hard to see**
- With multiple bots + multiple groups, its not obvious which agent is bound to which Telegram peer.
3) **Model IDs are easy to misconfigure**
- A wrong `provider/model-id` can silently degrade performance, cause timeouts, or produce “typing…” with no reply.
4) **Usage and troubleshooting take time**
- Without tooling, youre digging through JSONL sessions and gateway logs to answer: “who used tokens?”, “what failed?”, “which provider?”
OCMs role: **a zero-dependency local UI** that helps you view and update OpenClaw configuration safely, keep changes auditable, and centralize common ops.
---
## 1) Why Telegram groups are the best way to build sub-agents
Think of it as:
> **One Telegram group = one agent boundary** (context + purpose + workspace).
### Benefits
1) **Clear isolation**
- Each agent speaks only inside its own group.
- Less cross-contamination between tasks.
2) **Controllable context**
- The group chat is the agents input stream.
- You can keep an agent “clean” by keeping the group focused.
3) **Natural delegation**
- A main agent can coordinate; sub-agents can specialize (tech / travel / finance / translation, etc.).
4) **Auditable**
- You can see exactly which message triggered which response.
### Safety / cost: keep groups private
Recommended rule:
- **Each agent group should contain only you + the bot** (and optionally your second account).
- Dont invite other people:
- cost: anyone can trigger token usage
- safety: higher risk of prompt-injection / accidental tool actions
---
## 2) Start OCM (macOS)
From the repo directory:
```bash
bash start.sh
```
Then open: <http://localhost:3333>
Restrict to localhost only:
```bash
bash start.sh --host 127.0.0.1
```
---
## 3) Dashboard: check health first
The Dashboard is your “sanity check”:
- CPU / RAM / Disk
- gateway running status
- agent counts
Screenshot:
![](redacted-screenshots/dashboard.jpg)
---
## 4) Create a main agent (with its own Telegram bot)
> Your `main` agent usually exists already. This section is for creating an additional **root agent** that owns its own Telegram bot/account.
### 4.1 BotFather prerequisites
1) Create a bot
- In Telegram, open **BotFather**
- Run `/newbot` → follow prompts → copy the bot token
2) Allow groups
- BotFather → your bot → Bot Settings
- **Allow Groups = ON**
3) Disable group privacy (critical)
- BotFather → your bot → Bot Settings → Group Privacy
- **Group Privacy = OFF**
If group privacy is ON, the bot cant see normal group messages. The symptom often looks like:
- slow replies
- “typing…” appears frequently
- sometimes no response at all
### 4.2 Add the agent in OCM
Go to **Agents**:
![](redacted-screenshots/agents.jpg)
Click `+ Add Agent` and fill:
- agent id / name
- workspace path (recommended: separate folder per agent)
- model (dropdown is sourced from `openclaw models list`)
- the BotFather token
OCM writes the agent + Telegram account/binding into `openclaw.json`.
---
## 5) Create a sub-agent (recommended workflow)
### 5.1 Why a new group per sub-agent
- clean context isolation
- easier debugging
- easy to “turn off” by muting a group
### 5.2 Step-by-step
In **Agents**`+ Add Sub-Agent`:
**Step 1 — Create a Telegram group**
- private group
- add only: you + the bot
**Step 2 — Get the Group ID (peer id)**
Use gateway logs:
```bash
openclaw gateway logs --follow
```
Send a message in the new group. In logs you should see something like:
- `-100xxxxxxxxxx` (Telegram group id)
**Step 3 — Fill the sub-agent form**
- choose the Parent Agent (which bot/account to share)
- paste the Group ID
- set workspace (recommended: its own folder)
- choose model
**Step 4 — Allowlist (optional but recommended)**
If you use `channels.telegram.allowFrom`, OCM can take “Your Telegram User ID” and append it automatically to the allowlist.
---
## 6) Verify bindings (Channels)
Go to **Channels**:
![](redacted-screenshots/channels.jpg)
Here you can quickly validate:
- which agent is bound to which Telegram peer
- whether `main` has the catch-all binding (if you use that pattern)
---
## 7) Model settings (Models)
Go to **Models**:
![](redacted-screenshots/models.jpg)
Key idea:
- OCM uses the real CLI model list (`openclaw models list`) for dropdowns.
- This reduces “invalid model-id” mistakes.
---
## 8) Credentials (Auth)
Go to **Auth**:
![](redacted-screenshots/auth.jpg)
Use this page to:
- see which provider profiles exist
- troubleshoot expired/missing tokens
---
## 9) Usage stats (Stats)
Go to **Stats**:
![](redacted-screenshots/stats.jpg)
You can break down usage by:
- model
- agent
- time window
OCM parses usage from OpenClaw session JSONL files.
---
## 10) Cron jobs (Cron)
Go to **Cron**:
![](redacted-screenshots/cron.jpg)
You can:
- see scheduled tasks (backup / update / health)
- run tasks manually
- enable/disable entries
---
## 11) Built-in CLI Terminal
OCM includes a built-in terminal panel to run OpenClaw CLI commands directly (for example: `openclaw status`, `openclaw gateway logs --follow`, `openclaw doctor`).
How to open: click **⌨️ Terminal** in the top navigation, or use the bottom CLI panel.
Highlights:
- **Tab completion** while typing commands
- **Common commands** dropdown to insert templates quickly
- **Favorites**: save frequently used commands and run them with one click
- **Streaming output** (stdout/stderr) for faster debugging
Screenshot:
![](redacted-screenshots/cli.jpg)
## 11) Fast troubleshooting
### “Typing…” but no reply in a Telegram group
Check (in order):
1) BotFather: **Group Privacy = OFF**
2) `openclaw status` (gateway healthy?)
3) provider auth / rate limit issues (Auth page + gateway logs)
### Model dropdown doesnt show a model you expect
- dropdown comes from `openclaw models list`
- ensure the model is registered/visible in your OpenClaw environment
---
## Notes about screenshots
All screenshots in this guide are **redacted** (no personal paths, no Telegram IDs).
They live under `docs/redacted-screenshots/`.
+267
View File
@@ -0,0 +1,267 @@
# OCM 功能使用说明(含截图)
> 面向:已经在本机安装并跑通 OpenClaw 的用户(至少会用 `openclaw status` / `openclaw gateway logs`
>
> 目标:用 **OpenClaw Manager (OCM)** 把“创建 Agent / Sub-Agent、绑定 Telegram 群、管理模型/认证、看用量、看 cron”这套流程做成可视化、可回溯、低风险。
---
## 0) 先说痛点:为什么要用 OCM
### 0.1 创建/维护 Agent 的典型痛点
1. **改 `openclaw.json` 容易改错**
- 字段多、层级深、容易手滑覆盖 token / accountId / bindings。
- 一次写错可能导致多个 Agent 全挂。
2. **Sub-Agent 的边界和“谁在用哪个 bot token”不直观**
- 多 Bot / 多 agent / 多群组绑定时,光靠肉眼看 JSON 很难确认拓扑结构是否正确。
3. **模型选择很容易“写了一个不存在的 model-id”**
- provider/model-id 拼错、或 provider 侧不支持,最后表现就是 Agent 变慢/报错/一直 typing。
4. **统计用量和排查问题耗时**
- 需要翻 session JSONL / gateway logs 才能知道谁在烧 token、哪里失败。
OCM 的定位:**把配置变更“可视化 + 可检查 + 可回滚(配合备份)”,并把常用运维入口集中起来。**
---
## 1) 为什么用 Telegram group 来建 Sub-Agent 最合适
> 这里讨论的是 OpenClaw 的一种非常实用的工作方式:**“一个 Telegram 群 = 一个 Agent 的工作空间/人格/上下文边界”**。
### 1.1 Telegram group 方案的优势
1. **边界天然清晰**
- 每个 Agent 只在自己的群里说话,和其他 Agent 隔离。
2. **上下文可控**
- 一个群的聊天记录就是该 Agent 的输入流;不会和别的任务混在一起。
3. **并发与分工自然**
- 主 Agent 负责总控/派工,Sub-Agent 负责专项:技术宅/旅行/理财/翻译……
4. **可审计**
- 群里发生了什么一眼可见;出问题也能定位是哪条消息触发。
### 1.2 为什么“群必须私密”(安全与成本)
- 强烈建议:**每个 Agent 群只留你自己 + 对应 bot(+ 必要时的另一个你自己的账号)**。
- 不要拉别人进来:
- 成本:别人一句话就触发 token 消耗
- 安全:可能诱导泄露信息/触发危险操作
---
## 2) 启动与进入 OCM
### 2.1 启动(macOS
在 OCM 目录:
```bash
bash start.sh
```
默认访问:<http://localhost:3333>
> 需要限制只能本机访问:`bash start.sh --host 127.0.0.1`
### 2.2 Dashboard(先看健康状态)
建议第一步先看 Dashboard,确认:
- 机器资源(CPU/RAM/DISK
- OpenClaw gateway 是否 Running
- Agents 数量
截图示例:
![](redacted-screenshots/dashboard.jpg)
---
## 3) 创建主 AgentMain Agent / 独立 Bot
> 你的 `main` 一般是默认存在的;这里重点讲“新增一个 **拥有独立 Telegram bot 的主 Agent**”。
### 3.1 BotFather 侧准备(必须)
1) 创建 bot
- 在 Telegram 找 **BotFather**
- 发送 `/newbot` → 按提示创建 → 拿到 bot token
2) 允许加群
- BotFather → 你的 bot → Bot Settings
- **Allow Groups = ON**
3) 关闭群隐私(非常关键)
- BotFather → 你的 bot → Bot Settings → Group Privacy
- **Group Privacy = OFF**
> 如果 Group Privacy 没关,bot 在群里看不到普通消息,会表现成“经常 typing 但没输出 / 没反应”。
### 3.2 在 OCM 添加主 Agent
进入 **Agents** 页面:
![](redacted-screenshots/agents.jpg)
点击 `+ Add Agent`,按表单填写:
- Agent id / name
- workspace 路径(建议独立目录)
- 选择模型(下拉来自 `openclaw models list`,更不容易选错)
- 填入刚拿到的 bot token
保存后,OCM 会把这颗主 Agent 写进 `openclaw.json`,并把 bot token 绑定到对应 `accountId`
---
## 4) 创建 Sub-Agent(最常用)
### 4.1 为什么 Sub-Agent 要用“新群”
- 让每个 Sub-Agent 有自己独立的对话上下文、任务边界
- 出问题直接在该群排查,不污染主 Agent 的对话
### 4.2 Sub-Agent 创建流程(逐步)
进入 **Agents** 页面 → 点击 `+ Add Sub-Agent`,按引导做:
#### Step 1:建 Telegram 群
- 新建群(私密)
- 群里只加:你自己 + 对应的 bot
#### Step 2:拿到群 IDPeer ID / Group ID
OpenClaw 通常会在 gateway 日志里打印群的 peer id。
常见拿法:
```bash
openclaw gateway logs --follow
```
然后在 Telegram 群里随便发一句话,观察日志。
> 你会看到类似 `-100xxxxxxxxxx` 这样的 group id。
#### Step 3:回到 OCM 填表
- Parent Agent(选择共享哪个 bot/account
- 填 Group ID
- workspace(建议单独目录)
- 选择模型
#### Step 4:写 allowlist(推荐)
如果你启用了 Telegram 的 allowlist(例如 `channels.telegram.allowFrom`),
OCM 的表单里可以填 “Your Telegram User ID”,让它自动把你的 user id 加进去,避免 DM/群消息被拒。
---
## 5) 绑定检查(Channels 页)
进入 **Channels**
![](redacted-screenshots/channels.jpg)
这里能一眼看到:
- 哪个 agent 绑定了哪个 Telegram peer(群)
- main 的“兜底绑定”(any)是否存在
> 如果某个 Sub-Agent 绑定错群/丢 binding,最直观的就是在这里对不上。
---
## 6) 模型管理(Models 页)
进入 **模型**
![](redacted-screenshots/models.jpg)
关键点:
- 主模型(primary)和 fallback 链
- 下拉列表来自 `openclaw models list`(能减少“选了不存在模型”的问题)
---
## 7) 认证管理(Auth 页)
进入 **认证**
![](redacted-screenshots/auth.jpg)
这里主要用来:
- 看当前有哪些 provider profile
- 排查某个 provider 是否过期/缺 token
---
## 8) 用量统计(Stats 页)
进入 **Stats**
![](redacted-screenshots/stats.jpg)
你可以按:
- 按模型聚合
- 按 Agent 聚合
- 按天趋势
> OCM 是从 OpenClaw 的 session JSONL 里解析真实 usage(比“看日志猜”靠谱)。
---
## 9) CLI 终端(内置)
OCM 内置一个 CLI 面板,用来直接运行 OpenClaw 命令(例如:`openclaw status``openclaw gateway logs --follow``openclaw doctor`)。
入口:点击顶部导航栏的 **⌨️ 终端**,或使用页面底部的 CLI 区域。
功能要点:
- **Tab 补全**:输入命令时按 Tab 进行补全/提示(适合 `openclaw ...` 子命令)
- **常用命令**:可从下拉选择,一键填充常用命令模板
- **收藏(Favorites)**:把常用命令保存为收藏,后续一键执行
- **实时输出**:执行后会流式显示 stdout/stderr,方便排错
截图示例:
![](redacted-screenshots/cli.jpg)
## 9) Cron(定时任务)
进入 **Cron**
![](redacted-screenshots/cron.jpg)
用途:
- 查看有哪些健康检查/更新/备份任务
- 手动运行
- 观察启用/禁用状态
---
## 10) 常见故障排查(快速)
### 10.1 Telegram 群里“一直 typing 但不回”
优先检查:
1) BotFather 的 **Group Privacy 是否 OFF**
2) OpenClaw gateway 是否健康(`openclaw status`
3) provider 是否限流/超时(Auth 页 + gateway logs
### 10.2 模型下拉没有你想要的模型
- 现在下拉是 `openclaw models list` 的真实输出。
- 先确保你在 OpenClaw 里已经注册/可见该模型。
---
## 11) 推荐的“最佳实践”
- 每个 Agent 都用独立 workspace(隔离文件/记忆)
- 每个 Sub-Agent 都用独立 Telegram 群(隔离上下文)
- 群里不要加第三方(成本 + 安全)
- 任何“改配置的大动作”尽量先备份(OCM/脚本/手工都行)
---
## 附:截图说明
本文截图来自 `docs/redacted-screenshots/`,已做脱敏(路径用户名、Telegram 绑定 ID 等均已模糊处理)。
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

+376 -126
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,25 +1861,28 @@ 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">
<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>
<!-- Right: Agent tree -->
<div class="agents-right">
<div class="agents-tree-wrap">
<div id="agentTree"><div class="empty" data-i18n="agents.empty">No Agents</div></div>
</div>
</div>
</div>
<!-- ══ Channels ══════════════════════════════════════════════ -->
<div class="panel" id="panel-channels">
@@ -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);
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) {
// 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);
}
});
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 += '<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 += 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>';
// 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>';
}
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>';
});
}
// 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'); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openclaw-manager",
"version": "0.5.0",
"version": "0.7.1",
"description": "A local web UI for managing OpenClaw AI agents — no npm install required",
"main": "openclaw-manager.js",
"scripts": {
+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 ""
}