22 Commits
Author SHA1 Message Date
taodeng 18b1b9529b ui: default language to English 2026-02-28 17:04:25 +10:00
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 1213 additions and 148 deletions
+96 -3
View File
@@ -1,7 +1,92 @@
# OpenClaw Manager — 开发日志 # OpenClaw Manager — 开发日志
> 最后更新:2026-02-26 > 最后更新:2026-02-27
> 当前版本:v0.6.5 > 当前版本: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 值 - **`assertBrowserScriptSyntax()`** 在启动时检查 MAIN_HTML_SCRIPT 的 evaluated 值
- **OpenClaw config 多 bot 格式**`channels.telegram.accounts.<id>.botToken`binding 用 `accountId` 路由 - **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] 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] HTTP 响应加 `Cache-Control: no-store` + version-based cache busting — done in v0.6.5
- [x] 彻底删除旧 `/api/agents/main` 端点残留(确认已清除)— confirmed 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: auto-refresh every 30s when tab is active
- [ ] Dashboard: OpenClaw version display (from `openclaw --version`) - [ ] 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 中文 → 逐步迁移为英文 - [ ] DEVLOG.md 中文 → 逐步迁移为英文
--- ---
+2 -2
View File
@@ -9,9 +9,9 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.openclaw.manager</string> <string>com.openclaw.manager</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.5.0</string> <string>0.7.1</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.5.0</string> <string>0.7.1</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleIconFile</key> <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. 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`. 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 ## 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. **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. **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. **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. **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. **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 ## Requirements
- Node.js 18+ ([download](https://nodejs.org/)) - Node.js 18+ ([download](https://nodejs.org/))
@@ -131,6 +184,22 @@ start.bat # Windows
- **运维面板** — 重启网关、查看日志、健康检查、本地/NAS 备份、Cron 任务管理 - **运维面板** — 重启网关、查看日志、健康检查、本地/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` 目录。如需指定其他路径: 启动脚本自动检测 `~/.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

+379 -127
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// ================================================================ // ================================================================
// OpenClaw Manager v0.6.5 // OpenClaw Manager v0.7.1
// 跨平台本地管理工具 (Windows / macOS / Linux) // 跨平台本地管理工具 (Windows / macOS / Linux)
// //
// 用法: // 用法:
@@ -24,7 +24,7 @@ const SCRIPT_DIR = __dirname;
const MANAGER_CONFIG = path.join(SCRIPT_DIR, 'manager-config.json'); const MANAGER_CONFIG = path.join(SCRIPT_DIR, 'manager-config.json');
let PORT = 3333; let PORT = 3333;
let HOST = '0.0.0.0'; let HOST = '0.0.0.0';
const APP_VERSION = '0.6.5'; const APP_VERSION = '0.7.1';
// --port 参数 // --port 参数
const portIdx = process.argv.indexOf('--port'); const portIdx = process.argv.indexOf('--port');
if (portIdx !== -1 && process.argv[portIdx + 1]) PORT = parseInt(process.argv[portIdx + 1]) || 3333; 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 终端控制码(光标移动、清行、颜色等) // 过滤 ANSI 终端控制码(光标移动、清行、颜色等)
function stripAnsi(str) { function stripAnsi(str) {
return str return str
@@ -345,15 +376,44 @@ async function handleApi(req, res, urlObj, body) {
const defaults = cfg.agents?.defaults || {}; const defaults = cfg.agents?.defaults || {};
const bindings = cfg.bindings || []; const bindings = cfg.bindings || [];
const groups = cfg.channels?.telegram?.groups || {}; 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 enriched = list.map(a => {
const binding = bindings.find(b => b.agentId === a.id && b.match?.peer?.kind === 'group'); const binding = bindings.find(b => b.agentId === a.id && b.match?.peer?.kind === 'group');
const groupId = binding?.match?.peer?.id || null; const groupId = binding?.match?.peer?.id || null;
const modelVal = a.model?.primary || (typeof a.model === 'string' ? a.model : 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 botBinding = bindings.find(b => b.agentId === a.id && b.match?.accountId && !b.match?.peer);
const hasOwnBot = botBinding ? true : false; // 'main' is always a root agent even without explicit binding
return { ...a, groupId, requireMention: groupId ? (groups[groupId]?.requireMention ?? true) : null, const isMain = a.id === 'main';
effectiveModel: modelVal || defaults.model?.primary || '默认', hasOwnBot }; 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.writeHead(200);
res.end(JSON.stringify({ agents: enriched, defaults })); 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 // POST /api/agents/bot — create agent with its own bot token
if (method === 'POST' && pathname === '/api/agents/bot') { 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()) { if (!botToken || !botToken.trim()) {
res.writeHead(400); res.end(JSON.stringify({ error: 'Bot Token is required' })); return; 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(wsPath, { recursive: true });
await fsp.mkdir(path.join(wsPath, 'memory'), { recursive: true }); await fsp.mkdir(path.join(wsPath, 'memory'), { recursive: true });
const agentName = name || agentId; 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'); 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.writeHead(200);
res.end(JSON.stringify({ res.end(JSON.stringify({
ok: true, agentId, workspacePath: wsPath, configBackup: bakPath, ok: true, agentId, workspacePath: wsPath, configBackup: bakPath,
notes: [ notes: [
'Agent created with its own bot token', 'Agent created with its own bot token',
'Workspace directory created with SOUL.md and MEMORY.md', 'Workspace directory created with SOUL.md and MEMORY.md',
'Runtime directory created at agents/' + agentId + '/',
'Configuration updated and backed up', 'Configuration updated and backed up',
'Changes take effect in ~300ms without restart' 'Restart gateway to load new bot: openclaw gateway restart'
] ]
})); }));
return; return;
@@ -435,7 +500,7 @@ async function handleApi(req, res, urlObj, body) {
// POST /api/agents — create sub-agent (shares parent bot) // POST /api/agents — create sub-agent (shares parent bot)
if (method === 'POST' && pathname === '/api/agents') { 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)) { 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; 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) cfg.channels.telegram = {};
if (!cfg.channels.telegram.groups) cfg.channels.telegram.groups = {}; if (!cfg.channels.telegram.groups) cfg.channels.telegram.groups = {};
cfg.channels.telegram.groups[gid] = { requireMention: false }; 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'); const bakPath = await writeConfig(cfg, 'create');
await fsp.mkdir(wsPath, { recursive: true }); await fsp.mkdir(wsPath, { recursive: true });
await fsp.mkdir(path.join(wsPath, 'memory'), { recursive: true }); await fsp.mkdir(path.join(wsPath, 'memory'), { recursive: true });
const name = displayName || agentId; const name = displayName || agentId;
await fsp.writeFile(path.join(wsPath, 'SOUL.md'), generateSoulMd(name, purpose, personality), 'utf8'); 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'); 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.writeHead(200);
res.end(JSON.stringify({ ok: true, agentId, workspacePath: wsPath, configBackup: bakPath, res.end(JSON.stringify({ ok: true, agentId, workspacePath: wsPath, configBackup: bakPath,
notes: [ notes: [
'Sub-agent created and shares parent bot', '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', 'Configuration updated and backed up',
'Changes take effect in ~300ms without restart' 'Restart gateway to apply: openclaw gateway restart'
], ],
})); }));
return; return;
@@ -582,13 +659,15 @@ async function handleApi(req, res, urlObj, body) {
// GET /api/models // GET /api/models
if (method === 'GET' && pathname === '/api/models') { if (method === 'GET' && pathname === '/api/models') {
const cfg = await readConfig(); const cfg = await readConfig();
const modelList = await getKnownModelsFromCli();
res.writeHead(200); res.writeHead(200);
res.end(JSON.stringify({ res.end(JSON.stringify({
models: cfg.agents?.defaults?.models || {}, models: cfg.agents?.defaults?.models || {},
authProfiles: cfg.auth?.profiles || {}, authProfiles: cfg.auth?.profiles || {},
primaryModel: cfg.agents?.defaults?.model?.primary || '', primaryModel: cfg.agents?.defaults?.model?.primary || '',
fallbacks: cfg.agents?.defaults?.model?.fallbacks || [], fallbacks: cfg.agents?.defaults?.model?.fallbacks || [],
knownModels: KNOWN_MODELS, knownModels: modelList.knownModels || [],
modelListError:modelList.error || '',
authProviders: AUTH_PROVIDERS, authProviders: AUTH_PROVIDERS,
})); }));
return; return;
@@ -1222,6 +1301,25 @@ async function handleApi(req, res, urlObj, body) {
const cpus = os.cpus(); const cpus = os.cpus();
const cpuModel = cpus.length ? cpus[0].model.trim() : 'Unknown'; const cpuModel = cpus.length ? cpus[0].model.trim() : 'Unknown';
const cpuCores = cpus.length; 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) // Disk usage (best-effort, works on macOS/Linux)
let diskTotal = 0, diskUsed = 0, diskFree = 0; let diskTotal = 0, diskUsed = 0, diskFree = 0;
@@ -1270,9 +1368,21 @@ async function handleApi(req, res, urlObj, body) {
gatewayPing = code > 0 && code < 500; gatewayPing = code > 0 && code < 500;
} catch (_) {} } catch (_) {}
// Agent count & last activity // Agent count (main vs sub) & last activity
let agentCount = 0; let agentCount = 0, mainAgentCount = 0, subAgentCount = 0;
let lastActivity = null; 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 { try {
const sessionsBase = path.join(OPENCLAW_DIR, 'agents'); const sessionsBase = path.join(OPENCLAW_DIR, 'agents');
const agentDirs = await fsp.readdir(sessionsBase); const agentDirs = await fsp.readdir(sessionsBase);
@@ -1297,9 +1407,9 @@ async function handleApi(req, res, urlObj, body) {
res.writeHead(200); res.writeHead(200);
res.end(JSON.stringify({ res.end(JSON.stringify({
ok: true, 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 }, 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, ocmVersion: APP_VERSION,
serverTime: now, serverTime: now,
})); }));
@@ -1358,12 +1468,12 @@ async function handleApi(req, res, urlObj, body) {
} }
res.writeHead(404); res.writeHead(404);
res.end(JSON.stringify({ error: '未知 API 路径: ' + pathname })); res.end(JSON.stringify({ error: 'Unknown API path: ' + pathname }));
} }
// ── 安装向导 HTML ────────────────────────────────────────────── // ── 安装向导 HTML ──────────────────────────────────────────────
const SETUP_HTML = `<!DOCTYPE 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> <style>
*{box-sizing:border-box;margin:0;padding:0} *{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;} 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> </style></head>
<body><div class="box"> <body><div class="box">
<h1>🦀 OpenClaw Manager</h1> <h1>🦀 OpenClaw Manager</h1>
<p>首次运行,请指定你的 OpenClaw 数据目录(包含 openclaw.json 的文件夹)。</p> <p>First time setup — please specify your OpenClaw data directory (the folder containing openclaw.json).</p>
<label>OpenClaw 目录路径</label> <label>OpenClaw Directory Path</label>
<input id="dir" type="text" placeholder="例如: /Users/yourname/.openclaw ~/.openclaw"> <input id="dir" type="text" placeholder="e.g. /Users/yourname/.openclaw or ~/.openclaw">
<div class="hint">常见位置:<br>macOS / Linux<code>~/.openclaw</code><br>Windows<code>C:\\Users\\yourname\\.openclaw</code></div> <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> <div class="err" id="err"></div>
<button onclick="save()">确认并进入</button> <button onclick="save()">Confirm &amp; Enter</button>
</div> </div>
<script> <script>
async function save(){ async function save(){
const dir=document.getElementById('dir').value.trim(); const dir=document.getElementById('dir').value.trim();
const err=document.getElementById('err'); 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{ try{
const r=await fetch('/api/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dir})}); const r=await fetch('/api/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dir})});
const d=await r.json(); const d=await r.json();
if(d.ok){location.reload();}else{err.textContent=d.error||'路径无效';err.style.display='block';} if(d.ok){location.reload();}else{err.textContent=d.error||'Invalid path';err.style.display='block';}
}catch(e){err.textContent='请求失败:'+e.message;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();}); document.getElementById('dir').addEventListener('keydown',e=>{if(e.key==='Enter')save();});
</script></body></html>`; </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 { 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); } .inline-sel:focus { outline:none; border-color:var(--accent); }
/* Agents split layout */ /* Agents layout — buttons top, tree below */
.agents-split { display:flex; gap:20px; height:calc(100vh - 160px); } .agents-top-btns { display:flex; gap:10px; justify-content:center; margin-bottom:18px; }
.agents-left { flex:0 0 40%; display:flex; flex-direction:column; gap:12px; overflow-y:auto; } .agents-tree-wrap { max-width:900px; margin:0 auto; overflow-y:auto; }
.agents-right { flex:1; overflow-y:auto; padding-right:4px; } .agents-roots { display:flex; gap:16px; flex-wrap:wrap; }
.agents-left-btns { display:flex; gap:8px; } .agents-roots > .agent-tree-root { flex:1; min-width:320px; }
.agents-left-btns .btn-primary { flex:1; }
/* Add form */ /* Add form */
.add-form { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:16px; } .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: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-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; } .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 { 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:hover { border-color:var(--accent); }
.tree-child .tree-title { font-size:13px; font-weight:600; display:flex; align-items:center; gap:6px; } .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; } .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 ── */ /* ── 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 { padding:20px; }
.dash-card h3 { font-size:14px; font-weight:600; margin-bottom:14px; color:var(--text); } .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 { 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-row:last-child { border-bottom:none; }
.dash-label { color:var(--muted); } .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.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.stopped { background:#ef4444; box-shadow:0 0 6px rgba(239,68,68,.5); }
.dash-indicator.unknown { background:#f59e0b; } .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 ── */ /* ── Modal ── */
.backdrop { position:fixed; inset:0; background:rgba(0,0,0,.72); z-index:100; display:none; align-items:center; justify-content:center; } .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 ════════════════════════════════════════════ --> <!-- ══ Dashboard ════════════════════════════════════════════ -->
<div class="panel active" id="panel-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"> <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 class="dash-items" id="dashSysItems"><div class="empty">Loading...</div></div>
</div> </div>
<div class="card dash-card" id="dashGateway"> <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 class="dash-items" id="dashStorageItems"><div class="empty">Loading...</div></div>
</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> </div>
<!-- ══ Agents ════════════════════════════════════════════════ --> <!-- ══ Agents ════════════════════════════════════════════════ -->
<div class="panel" id="panel-agents"> <div class="panel" id="panel-agents">
<div class="agents-split"> <div class="agents-top-btns">
<!-- 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('agent')" data-i18n="agents.addAgent"> Add Agent</button>
<button class="btn-primary" onclick="showAddForm('sub')" data-i18n="agents.addSub"> Add Sub-Agent</button> <button class="btn-primary" onclick="showAddForm('sub')" data-i18n="agents.addSub"> Add Sub-Agent</button>
</div> </div>
<div id="addFormArea"></div> <div id="addFormArea"></div>
</div> <div class="agents-tree-wrap">
<!-- Right: Agent tree -->
<div class="agents-right">
<div id="agentTree"><div class="empty" data-i18n="agents.empty">No Agents</div></div> <div id="agentTree"><div class="empty" data-i18n="agents.empty">No Agents</div></div>
</div> </div>
</div> </div>
</div>
<!-- ══ Channels ══════════════════════════════════════════════ --> <!-- ══ Channels ══════════════════════════════════════════════ -->
<div class="panel" id="panel-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="panel" id="panel-models">
<div class="sec-hdr"><h2 data-i18n="models.title">模型管理</h2></div> <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-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 id="primaryModelWarn" class="warn-box" style="display:none;margin-bottom:10px"></div>
<div class="card" style="margin-bottom:12px"> <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> <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 与频道/群组的绑定关系。绑定顺序决定优先级。', 'channels.title':'Channel 绑定','channels.add':' 添加绑定','channels.hint':'管理 Agent 与频道/群组的绑定关系。绑定顺序决定优先级。',
'models.title':'模型管理','models.primary':'默认主模型','models.fallback':'Fallback 链', 'models.title':'模型管理','models.primary':'默认主模型','models.fallback':'Fallback 链',
'models.hint':'模型由 openclaw onboard 注册,此处管理主模型和 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.title':'认证配置','auth.configured':'已配置认证',
'auth.guide':'点击 Provider 查看认证步骤。认证需在终端完成,或使用下方 CLI 终端。', 'auth.guide':'点击 Provider 查看认证步骤。认证需在终端完成,或使用下方 CLI 终端。',
'auth.step1':'1. 获取 API Key','auth.step2':'2. 在终端运行以下命令','auth.step3':'3. 按提示粘贴 API Key 并回车', '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.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.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.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.s1':'在 BotFather 发送 <code>/newbot</code> 创建新 Bot,获取 Bot Token',
'guide.sub.s2':'Send a message in the group, find <code>peer.id</code> (negative number) in gateway logs', 'guide.sub.s2':'在 BotFather 发送 <code>/mybots</code> → 选择 Bot → <b>Bot Settings</b> → <b>Group Privacy</b> → <b>Turn off</b>',
'guide.sub.s3':'Fill in the Group ID and Agent config below', '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.empty':'暂无 Agent','agents.main':'主 Agent','agents.bound':'已绑群',
'agents.saveModel':'保存模型','agents.viewFiles':'查看文件', 'agents.saveModel':'保存模型','agents.viewFiles':'查看文件',
'agents.defaultModel':'使用全局默认','agents.custom':'自定义','agents.noModel':'默认', '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.', '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.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.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.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.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', '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.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.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.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.s1':'Send <code>/newbot</code> to BotFather to create a new Bot and get the Bot Token',
'guide.sub.s2':'Send a message in the group, find <code>peer.id</code> (negative number) in gateway logs', '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':'Fill in the Group ID and Agent config below', '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.empty':'No Agents','agents.main':'Main Agent','agents.bound':'Bound',
'agents.saveModel':'Save Model','agents.viewFiles':'View Files', 'agents.saveModel':'Save Model','agents.viewFiles':'View Files',
'agents.defaultModel':'Use Global Default','agents.custom':'custom','agents.noModel':'Default', 'agents.defaultModel':'Use Global Default','agents.custom':'custom','agents.noModel':'Default',
@@ -2319,7 +2484,9 @@ const LS = {
set(k, v) { try { localStorage.setItem(k, v); } catch(e) {} }, set(k, v) { try { localStorage.setItem(k, v); } catch(e) {} },
del(k) { try { localStorage.removeItem(k); } catch(e) {} }, del(k) { try { localStorage.removeItem(k); } catch(e) {} },
}; };
let lang = LS.get('ocm_lang', 'zh'); // Default UI language. Users can toggle and we persist in localStorage.
// Prefer English by default for broader sharing/promotions.
let lang = LS.get('ocm_lang', 'en');
function t(k) { return I18N[lang][k] || I18N.zh[k] || k; } function t(k) { return I18N[lang][k] || I18N.zh[k] || k; }
function applyLang() { function applyLang() {
document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); }); document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); });
@@ -2371,7 +2538,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 wizCur = 1;
let logTimer = null; let logTimer = null;
let selectedAuthProv = null; let selectedAuthProv = null;
@@ -2391,24 +2558,56 @@ async function loadAll(){ await Promise.all([loadAgents(), loadModels(), loadCha
// ── Dashboard ───────────────────────────────────────────────── // ── Dashboard ─────────────────────────────────────────────────
let dashLoaded=false; 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 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 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 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(){ async function loadDashboard(){
try{ try{
const r=await api('GET','/api/dashboard'); const r=await api('GET','/api/dashboard');
if(!r.ok)return; if(!r.ok)return;
const s=r.system, g=r.gateway, a=r.agents; 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 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)); let sysHtml=dashRow('Hostname',esc(s.hostname));
sysHtml+=dashRow('OS',esc(s.platform)); sysHtml+=dashRow('OS',esc(s.platform));
sysHtml+=dashRow('Node.js',esc(s.nodeVer)); 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('Uptime',fmtUptime(s.uptime));
sysHtml+=dashRow('Memory',fmtBytes(s.totalMem-s.freeMem)+' / '+fmtBytes(s.totalMem)+' ('+memPct.toFixed(0)+'%)'); sysHtml+=dashRow('Load Avg (1/5/15m)',loadStr);
sysHtml+=dashBar(memPct);
document.getElementById('dashSysItems').innerHTML=sysHtml; document.getElementById('dashSysItems').innerHTML=sysHtml;
// Gateway card // Gateway card
const statusIcon='<span class="dash-indicator '+esc(g.status)+'"></span>'; const statusIcon='<span class="dash-indicator '+esc(g.status)+'"></span>';
@@ -2419,7 +2618,9 @@ async function loadDashboard(){
gwHtml+=dashRow('HTTP Ping',g.ping?'<span style="color:#22c55e">✓ Reachable</span>':'<span style="color:#ef4444">✗ Unreachable</span>'); gwHtml+=dashRow('HTTP Ping',g.ping?'<span style="color:#22c55e">✓ Reachable</span>':'<span style="color:#ef4444">✗ Unreachable</span>');
document.getElementById('dashGwItems').innerHTML=gwHtml; document.getElementById('dashGwItems').innerHTML=gwHtml;
// Agents card // 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){ if(a.lastActivity){
const d=new Date(a.lastActivity); const d=new Date(a.lastActivity);
agHtml+=dashRow('Last Activity',d.toLocaleString('en-AU',{timeZone:'Australia/Brisbane',hour12:false})); agHtml+=dashRow('Last Activity',d.toLocaleString('en-AU',{timeZone:'Australia/Brisbane',hour12:false}));
@@ -2430,13 +2631,8 @@ async function loadDashboard(){
agHtml+=dashRow('Server Time',esc(r.serverTime)); agHtml+=dashRow('Server Time',esc(r.serverTime));
document.getElementById('dashAgentItems').innerHTML=agHtml; document.getElementById('dashAgentItems').innerHTML=agHtml;
// Storage card // Storage card
let stHtml=dashRow('OpenClaw Dir Size',fmtBytes(s.dirSize)); let stHtml=dashRow('OpenClaw Dir',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)); stHtml+=dashRow('Disk Free',fmtBytes(s.diskFree));
}
document.getElementById('dashStorageItems').innerHTML=stHtml; document.getElementById('dashStorageItems').innerHTML=stHtml;
dashLoaded=true; dashLoaded=true;
}catch(e){console.error('Dashboard load error:',e);} }catch(e){console.error('Dashboard load error:',e);}
@@ -2480,6 +2676,7 @@ async function loadModels(){
const r=await api('GET','/api/models'); const r=await api('GET','/api/models');
S.models=r.models||{}; S.authProfiles=r.authProfiles||{}; S.models=r.models||{}; S.authProfiles=r.authProfiles||{};
S.knownModels=r.knownModels||[]; S.authProviders=r.authProviders||[]; S.knownModels=r.knownModels||[]; S.authProviders=r.authProviders||[];
S.modelListError=r.modelListError||'';
S.primaryModel=r.primaryModel||''; S.fallbacks=r.fallbacks||[]; S.primaryModel=r.primaryModel||''; S.fallbacks=r.fallbacks||[];
renderModels(); renderAuth(); buildModelDropdowns(); renderModels(); renderAuth(); buildModelDropdowns();
}catch(e){ toast('加载模型失败: '+e.message,'error'); } }catch(e){ toast('加载模型失败: '+e.message,'error'); }
@@ -2528,6 +2725,11 @@ function buildAddAgentForm() {
'<div class="form-group"><label>' + t('wiz.model') + '</label>' + '<div class="form-group"><label>' + t('wiz.model') + '</label>' +
'<select id="fa-model">' + modelOpts + '</select>' + '<select id="fa-model">' + modelOpts + '</select>' +
'</div>' + '</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">' + '<div style="display:flex;gap:8px;margin-top:14px">' +
'<button class="btn-primary" onclick="submitAddAgent()">' + t('agents.addAgentSubmit') + '</button>' + '<button class="btn-primary" onclick="submitAddAgent()">' + t('agents.addAgentSubmit') + '</button>' +
'<button class="btn-ghost" onclick="clearAddForm()">' + t('btn.cancel') + '</button>' + '<button class="btn-ghost" onclick="clearAddForm()">' + t('btn.cancel') + '</button>' +
@@ -2553,12 +2755,19 @@ function buildAddSubForm() {
'<li>' + t('guide.sub.s1') + '</li>' + '<li>' + t('guide.sub.s1') + '</li>' +
'<li>' + t('guide.sub.s2') + '</li>' + '<li>' + t('guide.sub.s2') + '</li>' +
'<li>' + t('guide.sub.s3') + '</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>' + '<div class="form-group"><label>Parent Agent</label>' +
'<select id="fs-parent">' + parentOpts + '</select></div>' + '<select id="fs-parent">' + parentOpts + '</select></div>' +
'<div class="form-group"><label>' + t('wiz.groupId') + '</label>' + '<div class="form-group"><label>' + t('wiz.groupId') + '</label>' +
'<input id="fs-gid" placeholder="-100XXXXXXXXXX">' + '<input id="fs-gid" placeholder="-100XXXXXXXXXX">' +
'<span class="hint-text">' + t('wiz.groupHint') + '</span></div>' + '<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-row">' +
'<div class="form-group"><label>' + t('wiz.agentId') + ' <span style="color:var(--muted);font-weight:400">' + t('wiz.agentIdHint') + '</span></label>' + '<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>' + '<input id="fs-aid" placeholder="' + t('wiz.agentIdPh') + '"></div>' +
@@ -2585,13 +2794,15 @@ async function submitAddAgent() {
const name = document.getElementById('fa-name').value.trim(); const name = document.getElementById('fa-name').value.trim();
const workspace = document.getElementById('fa-workspace').value.trim(); const workspace = document.getElementById('fa-workspace').value.trim();
const model = document.getElementById('fa-model').value; 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 (!token) { toast(t('agents.errToken'), 'err'); return; }
if (!agentId) { toast('Agent ID is required', '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 (!/^[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 (!name) { toast(t('agents.errName'), 'err'); return; }
if (!workspace) { toast('Workspace name is required', 'err'); return; } if (!workspace) { toast('Workspace name is required', 'err'); return; }
try { 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); const r = await api('POST', '/api/agents/bot', payload);
toast('Agent created successfully', 'ok'); toast('Agent created successfully', 'ok');
closePopover(); closePopover();
@@ -2606,6 +2817,7 @@ async function submitAddAgent() {
async function submitAddSub() { async function submitAddSub() {
const parentAgentId = document.getElementById('fs-parent').value.trim(); const parentAgentId = document.getElementById('fs-parent').value.trim();
const groupId = document.getElementById('fs-gid').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 agentId = document.getElementById('fs-aid').value.trim();
const displayName = document.getElementById('fs-name').value.trim(); const displayName = document.getElementById('fs-name').value.trim();
const model = document.getElementById('fs-model').value; const model = document.getElementById('fs-model').value;
@@ -2616,8 +2828,9 @@ async function submitAddSub() {
if (!groupId) { toast(t('wiz.errGroupId'), 'err'); return; } if (!groupId) { toast(t('wiz.errGroupId'), 'err'); return; }
if (!agentId) { toast(t('wiz.errAgentId'), 'err'); return; } if (!agentId) { toast(t('wiz.errAgentId'), 'err'); return; }
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) { toast(t('wiz.errIdFormat'), '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 { 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; } if (r.error) { toast(r.error, 'err'); return; }
toast(t('wiz.created'), 'ok'); toast(t('wiz.created'), 'ok');
document.getElementById('addFormArea').innerHTML = ''; document.getElementById('addFormArea').innerHTML = '';
@@ -2631,63 +2844,93 @@ function renderAgents() {
const el = document.getElementById('agentTree'); const el = document.getElementById('agentTree');
if (!S.agents.length) { el.innerHTML = '<div class="empty">' + t('agents.empty') + '</div>'; return; } if (!S.agents.length) { el.innerHTML = '<div class="empty">' + t('agents.empty') + '</div>'; return; }
// Build tree: main agent as root, subagents as children // Build tree: each hasOwnBot agent is a root, others are sub-agents grouped by parentAccountId
const mainAgent = S.agents.find(a => a.id === 'main'); const roots = S.agents.filter(a => a.hasOwnBot);
const subs = S.agents.filter(a => a.id !== 'main'); 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 // Group subs under their parent
if (mainAgent) { 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="agent-tree-root">';
html += '<div class="tree-main">'; html += agentCard(root, true);
html += '<div class="tree-title">🤖 ' + esc(mainAgent.name || 'main') + ' <span class="badge main">' + t('agents.main') + '</span></div>'; if (children.length) {
html += '<div class="tree-meta">🧠 ' + esc(mainAgent.effectiveModel) + '</div>'; const treeId = 'tree-' + root.id;
if (mainAgent.workspace) html += '<div class="tree-meta">📁 ' + esc(mainAgent.workspace) + '</div>'; html += '<div class="tree-children-wrap">';
html += '<div class="tree-actions">'; html += '<button class="tree-toggle" onclick="toggleTree(\\'' + treeId + '\\',this)" title="Expand / Collapse"></button>';
html += '<select class="inline-sel" id="msel-main" onchange="">' + buildModelOpts(mainAgent.effectiveModel !== t('agents.noModel') ? mainAgent.effectiveModel : '__default__') + '</select>'; html += '<div class="tree-children" id="' + treeId + '">';
html += '<button class="btn-secondary" onclick="saveAgentModel(\\'main\\')">' + t('agents.saveModel') + '</button>'; children.forEach(c => { html += agentCard(c, false); });
html += '</div></div>'; 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>'; 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; 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){ function buildModelOpts(selected){
@@ -2702,10 +2945,6 @@ function buildModelOpts(selected){
opts+=\`<option value="\${m.id}"\${selected===m.id?' selected':''}>\${m.label}</option>\`; opts+=\`<option value="\${m.id}"\${selected===m.id?' selected':''}>\${m.label}</option>\`;
}); });
if(lastGroup) opts+=\`</optgroup>\`; 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; return opts;
} }
@@ -2771,6 +3010,19 @@ async function deleteChannel(idx,label){
// ── 渲染模型 ───────────────────────────────────────────────── // ── 渲染模型 ─────────────────────────────────────────────────
function renderModels(){ 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(显示修复警告) // 检测 primary model 是否是 API Key(显示修复警告)
const primWarn=document.getElementById('primaryModelWarn'); const primWarn=document.getElementById('primaryModelWarn');
if(primWarn){ if(primWarn){
@@ -2787,7 +3039,7 @@ function renderModels(){
} }
} }
const pSel=document.getElementById('primaryModelSel'); 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=>{ S.knownModels.filter(m=>m.id!=='__default__').forEach(m=>{
pSel.innerHTML+=\`<option value="\${m.id}"\${S.primaryModel===m.id?' selected':''}>\${m.label}</option>\`; pSel.innerHTML+=\`<option value="\${m.id}"\${S.primaryModel===m.id?' selected':''}>\${m.label}</option>\`;
}); });
@@ -2828,7 +3080,7 @@ async function savePrimaryModel(){
// 修复被错误设置为 API Key 的主模型 → 重置为第一个已注册模型或留空 // 修复被错误设置为 API Key 的主模型 → 重置为第一个已注册模型或留空
async function fixBadPrimaryModel(){ async function fixBadPrimaryModel(){
// 尝试从已注册模型中取第一个可用 ID // 尝试从已注册模型中取第一个可用 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 || ''; const resetTo = firstModel || '';
if(!confirm(\`将主模型重置为"\${resetTo||'(清空,使用全局默认)'}"\`)) return; if(!confirm(\`将主模型重置为"\${resetTo||'(清空,使用全局默认)'}"\`)) return;
try{ try{
@@ -2982,7 +3234,7 @@ async function refreshAuthOnly(){
try{ try{
const r=await api('GET','/api/models'); const r=await api('GET','/api/models');
S.authProfiles=r.authProfiles||{}; 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(); renderAuth(); buildModelDropdowns();
}catch(e){ toast((lang==='en'?'Refresh failed: ':'刷新失败: ')+e.message,'error'); } }catch(e){ toast((lang==='en'?'Refresh failed: ':'刷新失败: ')+e.message,'error'); }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "openclaw-manager", "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", "description": "A local web UI for managing OpenClaw AI agents — no npm install required",
"main": "openclaw-manager.js", "main": "openclaw-manager.js",
"scripts": { "scripts": {
+2 -2
View File
@@ -1,6 +1,6 @@
@echo off @echo off
:: ================================================================ :: ================================================================
:: OpenClaw Manager v0.6.0 — Start Script (Windows) :: OpenClaw Manager v0.7.1 — Start Script (Windows)
:: ::
:: Features: :: Features:
:: * Auto-detect Node.js, show install instructions if missing :: * Auto-detect Node.js, show install instructions if missing
@@ -19,7 +19,7 @@ set "MIN_NODE_MAJOR=18"
echo. echo.
echo =================================== echo ===================================
echo OpenClaw Manager v0.6.0 echo OpenClaw Manager v0.7.1
echo ----------------------------------- echo -----------------------------------
echo. echo.
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ================================================================ # ================================================================
# OpenClaw Manager v0.6.0 — Start Script (macOS / Linux) # OpenClaw Manager v0.7.1 — Start Script (macOS / Linux)
# #
# Features: # Features:
# - Auto-detect Node.js, show install instructions if missing # - 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() { banner() {
echo "" 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 -e "${DIM} ─────────────────────────────${RESET}"
echo "" echo ""
} }