Author SHA1 Message Date
taodengandClaude Opus 4.6 0338f599ce fix: improve agent tree ordering and parent inference
Root agents with their own bot (hasOwnBot) now sort to the top of the
agent tree alongside main. Agents listed in a parent's
subagents.allowAgents are automatically inferred as children even
without an explicit parentAgentId or Telegram binding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 20:33:03 +10:00
16a080e0b2 fix: add English i18n keys for Skills & Tools picker (#14)
The Skills & Tools picker added in v0.9.4 only had Chinese i18n
strings, so the panel showed Chinese text even in English mode.
Add English translations for wiz.skillsTools, wiz.skillsHint,
wiz.skillsSection, wiz.toolsSection, and the quick-action buttons.

Co-authored-by: Tao <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 20:21:24 +10:00
69258d8428 feat: auto-load default Skills & Tools for new agents (#12) (#13)
Add a Skills & Tools picker to the Agent/Sub-Agent creation forms so
new agents automatically get useful defaults (memory-continuity,
agent-workflow, execution-agent-dispatch, session-logs + fs/runtime/
memory/sessions_spawn/subagents tool groups).  Users can expand the
collapsible panel to customize, or leave it closed to accept defaults.

- Frontend: checkbox grid with quick-action buttons (Default/All/None)
- Backend: all 4 creation endpoints accept and persist skills/toolGroups
- Bump version to v0.9.4; update DEVLOG, CHANGELOG, README, guides

Co-authored-by: Tao <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 20:17:01 +10:00
a371ba6f7c docs: add related projects links (#12)
Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-03-08 16:09:47 +10:00
8ae5fdb20c bootstrap: create CURRENT_STATE for new agents (#11)
Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-08 11:30:36 +10:00
c3ed4c9c8f ui: add CLI copy helper + bump version to 0.9.2 (#10)
Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-08 10:20:34 +10:00
0a5f963b14 ui: add Dashboard first-run checklist card (#9)
Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-08 07:52:03 +10:00
9e3ec7b616 docs: switch README screenshots to hero layout A (#8)
Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-07 20:32:31 +10:00
55bfdcdbc8 ui: make CLI completion messages English (#7)
Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-07 20:29:28 +10:00
c6da627fed release: bump version to v0.9.1 (#6)
Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-07 20:19:00 +10:00
743b8acea1 docs: improve README positioning and refresh hero screenshots (#5)
* docs: rewrite README around first-run value and support positioning

* docs: add screenshot refresh brief and trim README repetition

* docs: refresh README hero screenshots

---------

Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-07 20:17:49 +10:00
e0ebe68fb5 docs/ux: startup polish and channel support follow-ups (#4)
* Fix localized startup status text

* Improve startup UX and docs positioning

* Clean up guide section numbering

* Docs: merge remaining channel support notes

---------

Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-06 21:10:12 +10:00
1c894d4ee7 v0.9.0: Discord support in Agents wizard + UI/guide updates (#3)
* feat: Agents wizard for Telegram/Discord + OCM header version

* fix: main-html-script syntax (tree toggle)

* ui: centered version badge + update Telegram/Discord setup guides

* ui: center prominent version badge + update Telegram/Discord setup guides

* fix: center OCM-only version badge + include ocmVersion in status + discord guide i18n

* fix: escape discord guide apostrophe in MAIN_HTML_SCRIPT

* release: v0.9.0

* fix: include ocmVersion in /api/status

* fix: add missing wiz.* i18n keys for Agents wizard

* fix: add wiz.* i18n keys to MAIN_HTML_SCRIPT

* docs: add Discord workflow section + new redacted screenshots (draft)

* docs: replace discord screenshots with English/redacted versions

* docs: replace discord add-subagent screenshot (English + redacted)

---------

Co-authored-by: Tao <dtzp555@gmail.com>
2026-03-01 21:57:01 +10:00
15 changed files with 696 additions and 119 deletions
+27 -2
View File
@@ -1,10 +1,35 @@
# OpenClaw Manager — 开发日志 # OpenClaw Manager — 开发日志
> 最后更新:2026-03-01 > 最后更新:2026-03-17
> 当前版本:v0.9.0 > 当前版本:v0.9.4
--- ---
## v0.9.4 更新日志(2026-03-17
### New Features
- **Skills & Tools 自动配置**:创建 Agent / Sub-Agent 时自动加载默认 Skills 和 Tool Groups
- 默认 Skills`memory-continuity`, `agent-workflow`, `execution-agent-dispatch`, `session-logs`
- 默认 Tool Groups`group:fs`, `group:runtime`, `group:memory`, `sessions_spawn`, `subagents`
- 创建表单新增可折叠的 Skills & Tools 选择面板,支持 checkbox 多选
- 提供「仅默认」「全选」「清空」快捷按钮
- 不展开 / 不修改则自动使用默认预设,开箱即用
- 支持的可选 Skillsbrowser-use, github, gh-issues, coding-agent, execution-agent-planner, discord, weather, summarize, healthcheck
- 四个创建端点(Telegram Agent/Sub、Discord Agent/Sub)均支持 `skills` + `toolGroups` 字段
### Backend
- 新增 `applySkillsTools()` 服务端辅助函数,统一处理 skills/tools 的默认值回退逻辑
- `POST /api/agents/bot``POST /api/agents/discord``POST /api/agents/discord-sub``POST /api/agents` 均已适配
### UI
- Skills & Tools Picker:深色风格 checkbox 网格,嵌入创建表单底部
- 新增专用 CSS 样式(`.skills-picker``.sp-grid``.sp-item``.sp-actions`
- 新增 i18n 字符串支持中英文标签
---
## v0.9.0 更新日志(2026-03-01 ## v0.9.0 更新日志(2026-03-01
+218 -64
View File
@@ -1,40 +1,74 @@
# OpenClaw Manager (OCM) # OpenClaw Manager (OCM)
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 local control panel for [OpenClaw](https://github.com/anthropics/openclaw) that makes setup checks, agent management, health inspection, and recovery much easier — without living in JSON and terminal tabs all day.
[中文说明](#中文说明) · [中文使用说明(含截图)](docs/USAGE_GUIDE.zh-CN.md) · [English Guide (with screenshots)](docs/USAGE_GUIDE.en.md) [中文说明](#中文说明) · [中文使用说明(含截图)](docs/USAGE_GUIDE.zh-CN.md) · [English Guide (with screenshots)](docs/USAGE_GUIDE.en.md)
--- ---
## Quick Start ## Why OCM exists
```bash Running OpenClaw directly is powerful, but the day-to-day workflow can get messy fast:
# Clone
git clone https://github.com/dtzp555-max/ocm.git
cd ocm
# Run (no npm install needed) - `openclaw.json` is easy to break by hand
bash start.sh # macOS / Linux - agent / bot / channel bindings are hard to visualize
start.bat # Windows - health checks, logs, restarts, and rollback are scattered across terminal commands
- when something goes wrong, recovery is slower than it should be
# Update OCM gives you a **local control panel** for the things that become painful first:
git pull --ff-only - seeing your current setup clearly
``` - operating it more safely
- recovering faster when something breaks
Open [http://localhost:3333](http://localhost:3333) in your browser. For remote access, use `bash start.sh --host 0.0.0.0`. ## Is OCM for you?
## Docs
- English guide: docs/USAGE_GUIDE.en.md **OCM is a good fit if:**
- 中文使用说明: docs/USAGE_GUIDE.zh-CN.md - you already run OpenClaw locally
- FAQ: docs/FAQ.md - you want a visual control panel for agents, routing, logs, health, and backups
- Security notes: docs/SECURITY.md - you are tired of editing nested config by hand
- Changelog: docs/CHANGELOG.md - you want safer rollback / recovery nearby
**OCM is probably not the best starting point if:**
- you have not installed OpenClaw yet
- you expect every OS / channel workflow to be equally mature today
- you want a hosted cloud dashboard instead of a local tool
## Features ## What OCM helps you do
### 1) See your OpenClaw setup clearly
- inspect agents and sub-agents in a tree
- verify bindings and routing
- view models, auth, usage, and health in one place
### 2) Operate OpenClaw with less risk
- restart gateway
- inspect logs
- run OpenClaw commands from the built-in terminal
- make config changes with rollback nearby
### 3) Recover faster when things go wrong
- browse backups / snapshots
- roll back config safely
- keep troubleshooting and operational tools close to the same UI
## See it quickly
These are the highest-value surfaces for first-time users:
<p>
<img src="docs/redacted-screenshots-refresh/dashboard-2026-03-07.jpg" width="860" />
</p>
<p>
<img src="docs/redacted-screenshots-refresh/agents-2026-03-07.jpg" width="280" />
<img src="docs/redacted-screenshots-refresh/cli-2026-03-07.jpg" width="280" />
<img src="docs/redacted-screenshots/actions.jpg" width="180" />
</p>
More screenshots: see the [full usage guide](docs/USAGE_GUIDE.en.md) or the gallery below.
<details> <details>
<summary><b>Screenshots</b> (redacted: no personal paths, no Telegram IDs)</summary> <summary><b>Full screenshots gallery</b> (redacted: no personal paths, no Telegram IDs)</summary>
<p> <p>
<img src="docs/redacted-screenshots/dashboard.jpg" width="240" /> <img src="docs/redacted-screenshots/dashboard.jpg" width="240" />
@@ -52,7 +86,77 @@ Open [http://localhost:3333](http://localhost:3333) in your browser. For remote
</details> </details>
- **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. ## Quick Start (recommended path)
```bash
# Clone
git clone https://github.com/dtzp555-max/ocm.git
cd ocm
# Run (no npm install needed)
bash start.sh # macOS / Linux
start.bat # Windows
```
Then open [http://localhost:3333](http://localhost:3333).
For remote access, use:
```bash
bash start.sh --host 0.0.0.0
```
## First-run checklist
For a good first experience, do these first:
1. Confirm OCM found the correct OpenClaw directory
2. Open **Dashboard** and check gateway health
3. Open **Agents** / **Channels** and inspect bindings
4. Open the built-in **Terminal** and run one OpenClaw command
5. Check **Actions** / backup / rollback so recovery is nearby before you need it
## Current support status
**Best supported today**
- macOS / Linux
- local OpenClaw installations
- local health / logs / ops / backup workflows
- Telegram-first agent management workflows
**Supported, but not equally smooth in every case**
- Discord agent / thread workflows
- more advanced routing setups
- mixed environment / service layouts
**Not the primary path today**
- fully beginner-first OpenClaw onboarding
- every OS / shell / service combination
- WhatsApp-heavy sub-agent topology workflows
## Common friction points
The most common reasons OCM feels rough are not usually “the UI is broken”, but environment mismatch:
- OpenClaw itself is not fully healthy yet
- wrong OpenClaw directory
- path / permission issues
- different service/runtime layouts across machines
- channel workflow differences
That is why OCM should be thought of as a **control panel for an existing OpenClaw setup**, not a magic replacement for all installation complexity.
## Docs
- **Start here**: 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 by area
- **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. New agents automatically get default Skills and Tool Groups, or you can customize them via the built-in Skills & Tools picker.
- **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** — Available from any page (top-right **Terminal** + bottom dock). Run OpenClaw commands with **real-time streaming output**, **Tab completion**, built-in presets, and your own saved favorites. - **Built-in CLI** — Available from any page (top-right **Terminal** + bottom dock). Run OpenClaw commands with **real-time streaming output**, **Tab completion**, built-in presets, and your own saved favorites.
@@ -62,12 +166,29 @@ Open [http://localhost:3333](http://localhost:3333) in your browser. For remote
## What's New (v0.9.x) ## What's New (v0.9.x)
- **Skills & Tools auto-config (v0.9.4)**: new agents/sub-agents automatically get default Skills (memory-continuity, agent-workflow, execution-agent-dispatch, session-logs) and Tool Groups (fs, runtime, memory, sessions_spawn, subagents). A collapsible Skills & Tools picker in the creation form lets you customize per agent.
- **Discord support (v0.9.x)**: add agent/sub-agent flows for Discord (main agent binds channel; sub-agent binds thread). - **Discord support (v0.9.x)**: add agent/sub-agent flows for Discord (main agent binds channel; sub-agent binds thread).
- **README screenshots gallery**: all screenshots are now grouped into one collapsible section to keep the homepage short. - **README screenshots gallery**: screenshots grouped into a collapsible section so the homepage stays shorter.
- **Built-in CLI terminal**: run OpenClaw commands from any page with streaming output, presets, favorites, and Tab completion. - **Built-in CLI terminal**: run OpenClaw commands from any page with streaming output, presets, favorites, and Tab completion.
- **Ops actions**: restart gateway, view logs, health check, backups (local + NAS), and cron management. - **Ops actions**: restart gateway, view logs, health check, backups (local + NAS), and cron management.
- **Telegram-first workflow**: safer sub-agent setup flow + allowlist helper + warnings for group privacy. - **Telegram-first workflow**: safer sub-agent setup flow + allowlist helper + warnings for group privacy.
## Telegram Workflow & Safety
## Channel Support
OCM is a local UI for OpenClaw. Core features like agent trees, routing, models/auth, ops, backups, and the built-in CLI are channel-agnostic.
Current channel positioning:
- **Telegram** — best supported and most documented today
- **Discord** — supported; recommended for private channels/threads with strict allowlist
- **Feishu / Lark** — supported in broader OpenClaw ecosystems, but not a primary OCM workflow today
- **WhatsApp** — not recommended for sub-agent topology workflows
## Recommended Workflows
OCM is **Telegram-first, not Telegram-only**. Telegram is the smoothest path today, but OCM is also useful for local ops, Discord routing, model/auth management, and backups.
### Telegram Workflow & Safety
OCM is designed primarily for **Telegram-based OpenClaw workflows**: OCM is designed primarily for **Telegram-based OpenClaw workflows**:
@@ -113,6 +234,18 @@ You can also create `manager-config.json` manually:
`manager-config.json` is gitignored and won't be committed. `manager-config.json` is gitignored and won't be committed.
## First-Run Experience
The first thing new users need is confidence that OCM is pointed at the right OpenClaw directory and that the gateway is healthy. The Dashboard, built-in CLI, and health/ops actions are intended to make that first-run check fast and visible.
Recommended first demo flow:
1. Start OCM
2. Open Dashboard and confirm gateway health
3. Open Agents / Routing and inspect bindings
4. Run one CLI command from the built-in terminal
5. Try backup / rollback from Ops
## Remote Access ## Remote Access
By default OCM binds to `0.0.0.0` (all interfaces). Access it from another device on your LAN using the IP shown at startup. To restrict to localhost only: By default OCM binds to `0.0.0.0` (all interfaces). Access it from another device on your LAN using the IP shown at startup. To restrict to localhost only:
@@ -147,6 +280,12 @@ ocm/
- **Frontend**: Vanilla HTML/CSS/JS embedded in a template literal — no build step, no framework - **Frontend**: Vanilla HTML/CSS/JS embedded in a template literal — no build step, no framework
- **Architecture**: Single-file, zero-dependency, runs anywhere Node.js runs - **Architecture**: Single-file, zero-dependency, runs anywhere Node.js runs
## Related Projects
- [clawkeeper](https://github.com/dtzp555-max/clawkeeper) — OpenClaw memory operations kit for doctor / provider switch / backup / restore workflows.
- [execution-agent-planner](https://github.com/dtzp555-max/execution-agent-planner) — skill for deciding when work should stay with one execution agent or split across specialized workers.
- [gh-pr-release-flow](https://github.com/dtzp555-max/gh-pr-release-flow) — skill for PR-first GitHub repos where release work should happen after merge.
## License ## License
MIT MIT
@@ -155,7 +294,38 @@ MIT
## 中文说明 ## 中文说明
OpenClaw Manager 是一个零依赖的本地 Web 管理界面,用于可视化管理 [OpenClaw](https://github.com/anthropics/openclaw) AI 智能体。所有代码在一个 `.js` 文件中,只需 Node.js 18+,不需要 `npm install` OpenClaw ManagerOCM)是一个本地 OpenClaw 控制面板,用来更直观地管理 Agent、查看绑定关系、做健康检查、运行内置 CLI,并在改配置或排错时少踩坑
### OCM 解决什么问题
直接使用 OpenClaw 很强,但日常维护很容易变成:
-`openclaw.json` 容易手滑
- Agent / bot / 群 / 线程绑定关系很难一眼看清
- 健康检查、日志、重启、回滚散落在多个终端命令里
- 出问题时恢复链路不够顺手
OCM 的定位不是替代 OpenClaw,而是给现有 OpenClaw 环境加一个**本地控制台**。
### 适合谁
**适合:**
- 已经在本机跑通 OpenClaw 的用户
- 想更直观地管理 Agent / Routing / Health / Logs / Backups 的用户
- 不想频繁手改复杂 JSON 的用户
**暂时不太适合:**
- 还没装好 OpenClaw、希望 OCM 包办全部安装复杂度的新手
- 默认期待所有平台 / 所有 channel 路线都同样成熟的用户
- 想要云端托管产品而不是本地工具的用户
### OCM 最有价值的三件事
1. **看清你的 OpenClaw 结构**
- Agent 树、绑定关系、模型、认证、健康状态更直观
2. **更安全地做运维操作**
- 重启 gateway、看日志、跑 CLI、改配置、回滚更顺手
3. **出问题时恢复更快**
- 备份、回滚、排查入口都更近
### 快速开始 ### 快速开始
@@ -166,49 +336,33 @@ bash start.sh # macOS / Linux
start.bat # Windows start.bat # Windows
``` ```
访问 [http://localhost:3333](http://localhost:3333)。远程访问请使用 `bash start.sh --host 0.0.0.0` 打开 [http://localhost:3333](http://localhost:3333)。
### 功能 ### 第一次使用建议先做这几步
- **Agent 管理** — 添加主 Agent 和子 Agent,树状结构查看,内联切换模型,浏览工作区文件 1. 确认 OCM 找到了正确的 OpenClaw 目录
- **使用统计** — 从 OpenClaw 会话文件解析真实 Token 用量,按模型/Agent/日期维度展示 2. 打开 **Dashboard** 看 gateway 是否健康
- **模型与认证** — 管理注册模型列表,内置 Provider 认证引导 3. 打开 **Agents / Channels** 看绑定是否正确
- **内置终端** — 实时流式输出,预设命令,收藏夹,Tab 补全 4. 打开内置 **Terminal** 跑一条 OpenClaw 命令
- **运维面板** — 重启网关、查看日志、健康检查、本地/NAS 备份、Cron 任务管理 5. 看一眼 **Actions / 备份 / 回滚**,确保恢复路径就在手边
- **双语界面** — 中英文一键切换
### Telegram 场景与安全提示 ### 当前支持定位
OCM 主要面向 **Telegram 场景** **目前最好用的场景**
- macOS / Linux
- 本地 OpenClaw 环境
- 本地 health / logs / ops / backup 工作流
- Telegram-first 的 Agent 管理方式
- 通过群组绑定主 Agent,并在每条树下管理多个 Sub-Agent **支持,但不保证同样顺滑:**
- 让每个 Agent 拥有独立 `workspace``SOUL.md``MEMORY.md` - Discord workflow
- 通过可视化方式更新 `openclaw.json`,减少手动改配置风险 - 更复杂的 routing / service 布局
- 建议使用者已具备基础 OpenClaw CLI 操作经验(如 `onboard`、认证、查看网关日志) - 混合环境
关键安全设置(务必确认): ### 文档
- BotFather 里 **Allow Groups = ON** - 英文使用说明:docs/USAGE_GUIDE.en.md
- BotFather 里 **Group Privacy = OFF** - 中文使用说明:docs/USAGE_GUIDE.zh-CN.md
- 每个 Agent 群组只保留“你自己 + 对应 Agent/Sub-Agent” - FAQdocs/FAQ.md
- 不要邀请其他人进组(会带来安全和 API 费用风险) - 安全说明:docs/SECURITY.md
- 更新记录:docs/CHANGELOG.md
### 配置
启动脚本自动检测 `~/.openclaw` 目录。如需指定其他路径:
```bash
bash start.sh --dir /path/to/.openclaw --port 8080
```
也可手动创建 `manager-config.json`
```json
{ "dir": "~/.openclaw" }
```
## Discord (thread-first) notes
- Recommended: keep top-level agents in dedicated channels, and bind sub-agents to **threads** (one thread per task).
- To switch a Telegram group / Discord thread binding: go to **Routing** → remove the old binding → add a new binding with the new ID/link.
+5
View File
@@ -2,6 +2,11 @@
This is a user-facing summary derived from `DEVLOG.md`. This is a user-facing summary derived from `DEVLOG.md`.
## v0.9.4
- **Skills & Tools auto-config**: New agents/sub-agents now automatically get default Skills (memory-continuity, agent-workflow, execution-agent-dispatch, session-logs) and Tool Groups (fs, runtime, memory, sessions_spawn, subagents).
- Agent creation forms include a collapsible Skills & Tools picker with checkbox multi-select and quick-action buttons (Default / All / None).
- All 4 creation endpoints (Telegram/Discord × Agent/Sub-Agent) support `skills` and `toolGroups` fields.
## v0.9.0 ## v0.9.0
- Agents: Telegram + Discord support in Add Agent / Add Sub-Agent wizard (main agent binds channel; sub-agent binds thread). - Agents: Telegram + Discord support in Add Agent / Add Sub-Agent wizard (main agent binds channel; sub-agent binds thread).
- UI: top-center OCM version badge; improved Telegram/Discord setup guides. - UI: top-center OCM version badge; improved Telegram/Discord setup guides.
+9
View File
@@ -51,3 +51,12 @@ That said, your local OpenClaw config contains sensitive information. Follow the
- don't share raw screenshots without redaction - don't share raw screenshots without redaction
See: `docs/SECURITY.md`. See: `docs/SECURITY.md`.
## Q: 为什么 OCM 暂时不推荐 WhatsApp 的 sub-agent 工作流?
WhatsApp 在 bot / group 自动化、权限边界和稳定路由上限制更多。
OCM 的 sub-agent 模式依赖清晰的会话边界、独立路由和可控权限,因此目前更推荐 Telegram / Discord 这类更容易做隔离的渠道。
如果未来支持 WhatsApp,更可能先从通知或轻量交互开始,而不是多 sub-agent 群聊拓扑。
+79
View File
@@ -0,0 +1,79 @@
# OCM Screenshot Refresh Brief
Goal: refresh only the highest-value README screenshots first, not every screenshot.
## Redaction rules
Always hide or blur:
- Telegram / Discord IDs
- bindings / peer IDs / thread IDs
- local filesystem paths containing Tao's name
- account labels if they expose personal identifiers
- anything that looks like a secret/token
## Priority 1 screenshots
### 1) Dashboard hero
Purpose:
- show OCM as a real control panel
- support the README first-run / health story
Wanted state:
- Dashboard page open
- gateway looks healthy if possible
- cards readable
- top bar clean
- no obvious warning/noise unless it helps credibility
Frame guidance:
- browser window wide enough to show full dashboard naturally
- keep top navigation visible
- avoid cropping too tightly
### 2) Agents page
Purpose:
- show the core product value: visible agent topology / structure
Wanted state:
- main agent + several sub-agents visible
- structure easy to understand at a glance
- avoid visual clutter where possible
- keep model selectors/buttons visible, but not the focus
Frame guidance:
- enough width to show the tree clearly
- IDs and personal paths redacted
### 3) CLI / Terminal page
Purpose:
- show that OCM is not only a viewer; it can operate OpenClaw too
Wanted state:
- built-in terminal open
- a real command visible (recommended: `openclaw status` or `openclaw doctor`)
- real output visible
- do not use tutorial arrows / annotation bubbles
Frame guidance:
- include command input and some output
- avoid empty terminal state
- redact paths / personal names
## Priority 2 screenshots
- Actions menu
- Backup / rollback
- Channels
- Models
- Auth
- Cron
- Stats
## Selected refresh set (2026-03-07)
- Dashboard: `docs/redacted-screenshots-refresh/dashboard-2026-03-07.jpg`
- Agents: `docs/redacted-screenshots-refresh/agents-2026-03-07.jpg`
- CLI: `docs/redacted-screenshots-refresh/cli-2026-03-07.jpg`
- Keep using existing `docs/redacted-screenshots/actions.jpg` for now
## Notes
- Existing `actions.jpg` is already strong and can stay for now.
- The new Dashboard and Agents captures are preferred for README hero/overview use.
- The new CLI capture is preferred because it shows a real command and real output.
+22
View File
@@ -28,3 +28,25 @@ If you share screenshots publicly:
- blur any tokens/keys if visible - blur any tokens/keys if visible
This repo contains **redacted + annotated** screenshots under `docs/annotated-screenshots/`. This repo contains **redacted + annotated** screenshots under `docs/annotated-screenshots/`.
## Discord safety
Recommended defaults (especially for shared/community servers):
- Prefer **private channels / private threads** as agent boundaries
- Use a strict **allowlist** of channels/threads the bot may respond in
- Avoid giving memory-enabled or high-privilege agents broad public exposure
- Prefer least-privilege Discord permissions for the bot
## Feishu / Lark safety
Feishu is more enterprise-oriented and usually needs stricter permission design:
- Request the smallest permission scope possible
- Treat chat/group visibility as a security boundary
- Prefer separate chats/spaces for different agents to reduce context bleed
## WhatsApp note
WhatsApp is not a recommended target for sub-agent topology workflows. Constraints in bot/group automation make reliable multi-agent isolation and routing harder.
+55 -12
View File
@@ -2,7 +2,7 @@
> Audience: People who already have OpenClaw running locally (you can run `openclaw status` and read `openclaw gateway logs`). > 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**. > Goal: Use **OpenClaw Manager (OCM)** as an **OpenClaw control panel** for agent setup, routing, health checks, built-in CLI, and safer config changes.
--- ---
@@ -60,7 +60,18 @@ Recommended rule:
--- ---
## 2) Start OCM (macOS) ## 2) First-run mindset
For new users, the first win is not “configure everything” — it is:
1. Confirm OCM found the right OpenClaw directory
2. Confirm the gateway is healthy
3. Confirm you can inspect agents / bindings
4. Confirm the built-in CLI works
That is why Dashboard + Routing + built-in CLI are the most important first-run surfaces.
## 3) Start OCM (macOS)
From the repo directory: From the repo directory:
@@ -78,7 +89,7 @@ bash start.sh --host 127.0.0.1
--- ---
## 3) Dashboard: check health first ## 4) Dashboard: check health first
The Dashboard is your “sanity check”: The Dashboard is your “sanity check”:
@@ -92,11 +103,11 @@ Screenshot:
--- ---
## 4) Create a main agent (with its own Telegram bot) ## 5) 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. > 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 ### 5.1 BotFather prerequisites
1) Create a bot 1) Create a bot
- In Telegram, open **BotFather** - In Telegram, open **BotFather**
@@ -115,7 +126,7 @@ If group privacy is ON, the bot cant see normal group messages. The symptom o
- “typing…” appears frequently - “typing…” appears frequently
- sometimes no response at all - sometimes no response at all
### 4.2 Add the agent in OCM ### 5.2 Add the agent in OCM
Go to **Agents**: Go to **Agents**:
@@ -127,20 +138,21 @@ Click `+ Add Agent` and fill:
- workspace path (recommended: separate folder per agent) - workspace path (recommended: separate folder per agent)
- model (dropdown is sourced from `openclaw models list`) - model (dropdown is sourced from `openclaw models list`)
- the BotFather token - the BotFather token
- **Skills & Tools** (optional): expand the collapsible picker at the bottom to customize. By default, new agents get: `memory-continuity`, `agent-workflow`, `execution-agent-dispatch`, `session-logs` as Skills, and `group:fs`, `group:runtime`, `group:memory`, `sessions_spawn`, `subagents` as Tool Groups. Use the quick-action buttons (Default / All / None) to adjust.
OCM writes the agent + Telegram account/binding into `openclaw.json`. OCM writes the agent + Telegram account/binding + skills + tool groups into `openclaw.json`.
--- ---
## 5) Create a sub-agent (recommended workflow) ## 6) Create a sub-agent (recommended workflow)
### 5.1 Why a new group per sub-agent ### 6.1 Why a new group per sub-agent
- clean context isolation - clean context isolation
- easier debugging - easier debugging
- easy to “turn off” by muting a group - easy to “turn off” by muting a group
### 5.2 Step-by-step ### 6.2 Step-by-step
In **Agents**`+ Add Sub-Agent`: In **Agents**`+ Add Sub-Agent`:
@@ -165,6 +177,7 @@ Send a message in the new group. In logs you should see something like:
- paste the Group ID - paste the Group ID
- set workspace (recommended: its own folder) - set workspace (recommended: its own folder)
- choose model - choose model
- **Skills & Tools** (optional): same picker as Add Agent — defaults are pre-selected, expand to customize
**Step 4 — Allowlist (optional but recommended)** **Step 4 — Allowlist (optional but recommended)**
@@ -172,7 +185,37 @@ If you use `channels.telegram.allowFrom`, OCM can take “Your Telegram User ID
--- ---
## 6) Verify bindings (Channels)
## 6.5) Discord workflow (main agent + sub-agent)
OCM also supports Discord:
- **Main agent**: bind to a dedicated **Channel** (channelId)
- **Sub-agent**: bind to a dedicated **Thread** under that channel (threadId)
### 6.5.1 Add a Discord main agent
Go to **Agents**`+ Add Agent` → set **Channel = Discord**:
![](redacted-screenshots/agents-discord-add-agent.png)
Steps:
1) Create/select a dedicated Discord **channel** (private recommended)
2) Right-click channel → **Copy Link** → paste into the form (auto-parses channelId)
### 6.5.2 Add a Discord sub-agent (thread-first)
Go to **Agents**`+ Add Sub-Agent` → set **Channel = Discord (thread only)**:
![](redacted-screenshots/agents-discord-add-subagent.png)
Steps:
1) Create a **thread** under the main channel (one thread per task)
2) If it is a private thread, add the bot to the thread
3) Right-click thread → **Copy Link** → paste into the form (auto-parses threadId)
## 7) Verify bindings (Channels)
Go to **Channels**: Go to **Channels**:
@@ -184,7 +227,7 @@ Here you can quickly validate:
--- ---
## 7) Model settings (Models) ## 8) Model settings (Models)
Go to **Models**: Go to **Models**:
+57 -14
View File
@@ -2,7 +2,7 @@
> 面向:已经在本机安装并跑通 OpenClaw 的用户(至少会用 `openclaw status` / `openclaw gateway logs` > 面向:已经在本机安装并跑通 OpenClaw 的用户(至少会用 `openclaw status` / `openclaw gateway logs`
> >
> 目标: **OpenClaw Manager (OCM)** 把“创建 Agent / Sub-Agent、绑定 Telegram 群、管理模型/认证、看用量、看 cron”这套流程做成可视化、可回溯、低风险 > 目标: **OpenClaw Manager (OCM)** 当成 **OpenClaw 控制台** 来用:管理 Agent、查看 Routing、做健康检查、运行内置 CLI,并更安全地修改配置
--- ---
@@ -54,9 +54,20 @@ OCM 的定位:**把配置变更“可视化 + 可检查 + 可回滚(配合
--- ---
## 2) 启动与进入 OCM ## 2) 第一次使用时最该确认什么
### 2.1 启动(macOS 对新用户来说,第一步不是“先把所有配置都改完”,而是先确认四件事:
1. OCM 找到的是正确的 OpenClaw 目录
2. Gateway 是健康的
3. Agent / Routing 能正常查看
4. 内置终端可以正常跑命令
所以第一次使用时,最重要的页面其实是 Dashboard、Routing 和内置终端。
## 3) 启动与进入 OCM
### 3.1 启动(macOS
在 OCM 目录: 在 OCM 目录:
@@ -68,7 +79,7 @@ bash start.sh
> 需要限制只能本机访问:`bash start.sh --host 127.0.0.1` > 需要限制只能本机访问:`bash start.sh --host 127.0.0.1`
### 2.2 Dashboard(先看健康状态) ### 3.2 Dashboard(先看健康状态)
建议第一步先看 Dashboard,确认: 建议第一步先看 Dashboard,确认:
- 机器资源(CPU/RAM/DISK - 机器资源(CPU/RAM/DISK
@@ -81,11 +92,11 @@ bash start.sh
--- ---
## 3) 创建主 AgentMain Agent / 独立 Bot ## 4) 创建主 AgentMain Agent / 独立 Bot
> 你的 `main` 一般是默认存在的;这里重点讲“新增一个 **拥有独立 Telegram bot 的主 Agent**”。 > 你的 `main` 一般是默认存在的;这里重点讲“新增一个 **拥有独立 Telegram bot 的主 Agent**”。
### 3.1 BotFather 侧准备(必须) ### 4.1 BotFather 侧准备(必须)
1) 创建 bot 1) 创建 bot
- 在 Telegram 找 **BotFather** - 在 Telegram 找 **BotFather**
@@ -101,7 +112,7 @@ bash start.sh
> 如果 Group Privacy 没关,bot 在群里看不到普通消息,会表现成“经常 typing 但没输出 / 没反应”。 > 如果 Group Privacy 没关,bot 在群里看不到普通消息,会表现成“经常 typing 但没输出 / 没反应”。
### 3.2 在 OCM 添加主 Agent ### 4.2 在 OCM 添加主 Agent
进入 **Agents** 页面: 进入 **Agents** 页面:
@@ -112,19 +123,20 @@ bash start.sh
- workspace 路径(建议独立目录) - workspace 路径(建议独立目录)
- 选择模型(下拉来自 `openclaw models list`,更不容易选错) - 选择模型(下拉来自 `openclaw models list`,更不容易选错)
- 填入刚拿到的 bot token - 填入刚拿到的 bot token
- **Skills & Tools**(可选):展开表单底部的折叠面板可自定义。默认预设:Skills 包含 `memory-continuity``agent-workflow``execution-agent-dispatch``session-logs`Tool Groups 包含 `group:fs``group:runtime``group:memory``sessions_spawn``subagents`。提供「仅默认 / 全选 / 清空」快捷按钮。
保存后,OCM 会把这颗主 Agent 写进 `openclaw.json`,并把 bot token 绑定到对应 `accountId` 保存后,OCM 会把主 Agent + skills + tools 写进 `openclaw.json`,并把 bot token 绑定到对应 `accountId`
--- ---
## 4) 创建 Sub-Agent(最常用) ## 5) 创建 Sub-Agent(最常用)
### 4.1 为什么 Sub-Agent 要用“新群” ### 5.1 为什么 Sub-Agent 要用“新群”
- 让每个 Sub-Agent 有自己独立的对话上下文、任务边界 - 让每个 Sub-Agent 有自己独立的对话上下文、任务边界
- 出问题直接在该群排查,不污染主 Agent 的对话 - 出问题直接在该群排查,不污染主 Agent 的对话
### 4.2 Sub-Agent 创建流程(逐步) ### 5.2 Sub-Agent 创建流程(逐步)
进入 **Agents** 页面 → 点击 `+ Add Sub-Agent`,按引导做: 进入 **Agents** 页面 → 点击 `+ Add Sub-Agent`,按引导做:
@@ -148,6 +160,7 @@ openclaw gateway logs --follow
- 填 Group ID - 填 Group ID
- workspace(建议单独目录) - workspace(建议单独目录)
- 选择模型 - 选择模型
- **Skills & Tools**(可选):和 Add Agent 相同的选择器,默认预设已勾选,展开可自定义
#### Step 4:写 allowlist(推荐) #### Step 4:写 allowlist(推荐)
如果你启用了 Telegram 的 allowlist(例如 `channels.telegram.allowFrom`), 如果你启用了 Telegram 的 allowlist(例如 `channels.telegram.allowFrom`),
@@ -155,7 +168,37 @@ OCM 的表单里可以填 “Your Telegram User ID”,让它自动把你的 us
--- ---
## 5) 绑定检查(Channels 页)
## 6) Discord 工作流(主 Agent + Sub-Agent
OCM 也支持 Discord
- **主 Agent**:绑定到一个专用 **Channel**channelId
- **Sub-Agent**:绑定到该 Channel 下的专用 **Thread**threadIdthread-first
### 5.1 添加 Discord 主 Agent
进入 **Agents**`+ Add Agent` → 选择 **渠道 = Discord**
![](redacted-screenshots/agents-discord-add-agent.png)
步骤:
1) 创建/选择一个专用 Discord Channel(建议私密)
2) 右键 Channel → **Copy Link** → 粘贴到表单(自动解析 channelId)
### 5.2 添加 Discord Sub-AgentThread 优先)
进入 **Agents**`+ Add Sub-Agent` → 选择 **渠道 = Discord (thread only)**
![](redacted-screenshots/agents-discord-add-subagent.png)
步骤:
1) 在主 Channel 下新建 Thread(建议“一个任务一个 thread”)
2) 如果是 private thread,需要把 bot 拉进 thread
3) 右键 Thread → **Copy Link** → 粘贴到表单(自动解析 threadId)
## 7) 绑定检查(Channels 页)
进入 **Channels** 进入 **Channels**
@@ -169,7 +212,7 @@ OCM 的表单里可以填 “Your Telegram User ID”,让它自动把你的 us
--- ---
## 6) 模型管理(Models 页) ## 8) 模型管理(Models 页)
进入 **模型** 进入 **模型**
@@ -181,7 +224,7 @@ OCM 的表单里可以填 “Your Telegram User ID”,让它自动把你的 us
--- ---
## 7) 认证管理(Auth 页) ## 9) 认证管理(Auth 页)
进入 **认证** 进入 **认证**
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

+222 -25
View File
@@ -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.9.0'; const APP_VERSION = '0.9.4';
// --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;
@@ -63,6 +63,17 @@ function refreshPaths() {
CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json'); CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
} }
// ── Default Skills & Tool Groups for new agents ─────────────
const DEFAULT_SKILLS = ['memory-continuity', 'agent-workflow', 'execution-agent-dispatch', 'session-logs'];
const DEFAULT_TOOL_GROUPS = ['group:fs', 'group:runtime', 'group:memory', 'sessions_spawn', 'subagents'];
function applySkillsTools(agentEntry, skills, toolGroups) {
const s = (Array.isArray(skills) && skills.length > 0) ? skills : DEFAULT_SKILLS;
const tg = (Array.isArray(toolGroups) && toolGroups.length > 0) ? toolGroups : DEFAULT_TOOL_GROUPS;
agentEntry.skills = s;
agentEntry.tools = { alsoAllow: tg };
}
// ── 已知模型列表 ────────────────────────────────────────────── // ── 已知模型列表 ──────────────────────────────────────────────
const KNOWN_MODELS = [ const KNOWN_MODELS = [
{ id: '__default__', label: '使用全局默认模型' }, { id: '__default__', label: '使用全局默认模型' },
@@ -315,6 +326,28 @@ ${hasMemory ? initialMemory : '> 暂无初始记录。记忆将通过日常对
`; `;
} }
function generateCurrentStateMd() {
return `# CURRENT_STATE
_Last updated: TBD Australia/Brisbane_
## In Flight
- none
## Blocked / Waiting
- none
## Recently Finished
- none
## Next
- none
## Reset Summary
- No active summary yet.
`;
}
// ── 请求解析 ────────────────────────────────────────────────── // ── 请求解析 ──────────────────────────────────────────────────
function parseBody(req) { function parseBody(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -354,6 +387,7 @@ async function handleApi(req, res, urlObj, body) {
})(), })(),
primaryModel: cfg.agents?.defaults?.model?.primary || '未配置', primaryModel: cfg.agents?.defaults?.model?.primary || '未配置',
platform: process.platform, platform: process.platform,
ocmVersion: APP_VERSION,
})); }));
} catch (e) { res.writeHead(500); res.end(JSON.stringify({ ok: false, error: e.message })); } } catch (e) { res.writeHead(500); res.end(JSON.stringify({ ok: false, error: e.message })); }
return; return;
@@ -394,6 +428,12 @@ async function handleApi(req, res, urlObj, body) {
); );
// For main agent without explicit binding, infer its accountId from first unclaimed telegram account // 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 mainInferredAccountId = telegramAccounts.find(a => !claimedAccounts.has(a)) || telegramAccounts[0] || null;
// Build map: agentId -> parentAgentId inferred from subagents.allowAgents
const allowAgentsParentMap = {};
list.forEach(parent => {
const allowed = parent.subagents?.allowAgents || [];
allowed.forEach(childId => { if (!allowAgentsParentMap[childId]) allowAgentsParentMap[childId] = parent.id; });
});
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;
@@ -423,7 +463,9 @@ async function handleApi(req, res, urlObj, body) {
// Workspace: explicit per-agent, or defaults.workspace for main // Workspace: explicit per-agent, or defaults.workspace for main
const workspace = a.workspace || (isMain ? defaultWorkspace : null); const workspace = a.workspace || (isMain ? defaultWorkspace : null);
return { ...a, workspace, groupId, requireMention: groupId ? (groups[groupId]?.requireMention ?? true) : null, return { ...a, workspace, groupId, requireMention: groupId ? (groups[groupId]?.requireMention ?? true) : null,
effectiveModel: modelVal || defaults.model?.primary || '默认', hasOwnBot, accountId, parentAccountId, parentAgentId: a.parentAgentId || null, bindings: (bindings||[]).filter(b=>b.agentId===a.id).map(b=>({ effectiveModel: modelVal || defaults.model?.primary || '默认', hasOwnBot, accountId, parentAccountId,
parentAgentId: a.parentAgentId || (!hasOwnBot ? allowAgentsParentMap[a.id] : null) || null,
bindings: (bindings||[]).filter(b=>b.agentId===a.id).map(b=>({
idx: bindings.indexOf(b), idx: bindings.indexOf(b),
channel: b.match?.channel || '', channel: b.match?.channel || '',
accountId: b.match?.accountId || '', accountId: b.match?.accountId || '',
@@ -438,7 +480,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, purpose, personality } = body; const { botToken, agentId, name, model, workspace, purpose, personality, skills, toolGroups } = 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;
} }
@@ -484,6 +526,7 @@ async function handleApi(req, res, urlObj, body) {
const wsAlias = `~/.openclaw/workspaces/${workspace}`; const wsAlias = `~/.openclaw/workspaces/${workspace}`;
const agentEntry = { id: agentId, name: name || agentId, workspace: wsAlias }; const agentEntry = { id: agentId, name: name || agentId, workspace: wsAlias };
if (model && model !== '__default__') agentEntry.model = { primary: model }; if (model && model !== '__default__') agentEntry.model = { primary: model };
applySkillsTools(agentEntry, skills, toolGroups);
cfg.agents.list.push(agentEntry); cfg.agents.list.push(agentEntry);
// Add binding // Add binding
if (!cfg.bindings) cfg.bindings = []; if (!cfg.bindings) cfg.bindings = [];
@@ -496,6 +539,7 @@ async function handleApi(req, res, urlObj, body) {
const agentName = name || agentId; const agentName = name || agentId;
await fsp.writeFile(path.join(wsPath, 'SOUL.md'), generateSoulMd(agentName, purpose || '', personality || ''), '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');
await fsp.writeFile(path.join(wsPath, 'memory', 'CURRENT_STATE.md'), generateCurrentStateMd(), 'utf8');
// Create agents/<id>/ runtime directory (required by OpenClaw gateway) // Create agents/<id>/ runtime directory (required by OpenClaw gateway)
const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId); const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId);
await fsp.mkdir(agentRuntimeDir, { recursive: true }); await fsp.mkdir(agentRuntimeDir, { recursive: true });
@@ -505,7 +549,7 @@ async function handleApi(req, res, urlObj, body) {
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, MEMORY.md, and memory/CURRENT_STATE.md',
'Runtime directory created at agents/' + agentId + '/', 'Runtime directory created at agents/' + agentId + '/',
'Configuration updated and backed up', 'Configuration updated and backed up',
'Restart gateway to load new bot: openclaw gateway restart' 'Restart gateway to load new bot: openclaw gateway restart'
@@ -516,7 +560,7 @@ async function handleApi(req, res, urlObj, body) {
// POST /api/agents/discord — create top-level Discord agent (single bot, channel binding) // POST /api/agents/discord — create top-level Discord agent (single bot, channel binding)
if (method === 'POST' && pathname === '/api/agents/discord') { if (method === 'POST' && pathname === '/api/agents/discord') {
const { agentId, name, workspaceFolder, model, purpose, personality, guildId, channelId } = body; const { agentId, name, workspaceFolder, model, purpose, personality, guildId, channelId, skills, toolGroups } = 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;
} }
@@ -542,6 +586,7 @@ async function handleApi(req, res, urlObj, body) {
const wsPath = path.join(OPENCLAW_DIR, 'workspaces', folder); const wsPath = path.join(OPENCLAW_DIR, 'workspaces', folder);
const agentEntry = { id: agentId, name: name || agentId, workspace: wsAlias }; const agentEntry = { id: agentId, name: name || agentId, workspace: wsAlias };
if (model && model !== '__default__') agentEntry.model = { primary: model }; if (model && model !== '__default__') agentEntry.model = { primary: model };
applySkillsTools(agentEntry, skills, toolGroups);
cfg.agents.list.push(agentEntry); cfg.agents.list.push(agentEntry);
if (!cfg.bindings) cfg.bindings = []; if (!cfg.bindings) cfg.bindings = [];
@@ -564,6 +609,7 @@ async function handleApi(req, res, urlObj, body) {
await fsp.mkdir(path.join(wsPath, 'memory'), { recursive: true }); await fsp.mkdir(path.join(wsPath, 'memory'), { recursive: true });
await fsp.writeFile(path.join(wsPath, 'SOUL.md'), generateSoulMd(name || agentId, purpose || '', personality || ''), 'utf8'); await fsp.writeFile(path.join(wsPath, 'SOUL.md'), generateSoulMd(name || agentId, purpose || '', personality || ''), 'utf8');
await fsp.writeFile(path.join(wsPath, 'MEMORY.md'), generateMemoryMd(name || agentId, ''), 'utf8'); await fsp.writeFile(path.join(wsPath, 'MEMORY.md'), generateMemoryMd(name || agentId, ''), 'utf8');
await fsp.writeFile(path.join(wsPath, 'memory', 'CURRENT_STATE.md'), generateCurrentStateMd(), 'utf8');
const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId); const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId);
await fsp.mkdir(agentRuntimeDir, { recursive: true }); await fsp.mkdir(agentRuntimeDir, { recursive: true });
@@ -574,7 +620,7 @@ async function handleApi(req, res, urlObj, body) {
notes: [ notes: [
'Discord agent created (single Discord bot)', 'Discord agent created (single Discord bot)',
'Channel binding added', 'Channel binding added',
'Workspace + runtime directories created', 'Workspace + runtime directories created (including memory/CURRENT_STATE.md)',
'Configuration updated and backed up', 'Configuration updated and backed up',
'Restart gateway to apply: openclaw gateway restart' 'Restart gateway to apply: openclaw gateway restart'
] ]
@@ -584,7 +630,7 @@ async function handleApi(req, res, urlObj, body) {
// POST /api/agents/discord-sub — create Discord sub-agent (thread-only binding, grouping under parentAgentId) // POST /api/agents/discord-sub — create Discord sub-agent (thread-only binding, grouping under parentAgentId)
if (method === 'POST' && pathname === '/api/agents/discord-sub') { if (method === 'POST' && pathname === '/api/agents/discord-sub') {
const { agentId, displayName, workspaceFolder, model, purpose, personality, initialMemory, parentAgentId, guildId, threadId } = body; const { agentId, displayName, workspaceFolder, model, purpose, personality, initialMemory, parentAgentId, guildId, threadId, skills, toolGroups } = 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;
} }
@@ -610,6 +656,7 @@ async function handleApi(req, res, urlObj, body) {
const wsPath = path.join(OPENCLAW_DIR, 'workspaces', folder); const wsPath = path.join(OPENCLAW_DIR, 'workspaces', folder);
const agentEntry = { id: agentId, name: displayName || agentId, workspace: wsAlias, parentAgentId: String(parentAgentId).trim() }; const agentEntry = { id: agentId, name: displayName || agentId, workspace: wsAlias, parentAgentId: String(parentAgentId).trim() };
if (model && model !== '__default__') agentEntry.model = { primary: model }; if (model && model !== '__default__') agentEntry.model = { primary: model };
applySkillsTools(agentEntry, skills, toolGroups);
cfg.agents.list.push(agentEntry); cfg.agents.list.push(agentEntry);
if (!cfg.bindings) cfg.bindings = []; if (!cfg.bindings) cfg.bindings = [];
@@ -633,6 +680,7 @@ async function handleApi(req, res, urlObj, body) {
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');
await fsp.writeFile(path.join(wsPath, 'memory', 'CURRENT_STATE.md'), generateCurrentStateMd(), 'utf8');
const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId); const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId);
await fsp.mkdir(agentRuntimeDir, { recursive: true }); await fsp.mkdir(agentRuntimeDir, { recursive: true });
@@ -642,7 +690,7 @@ async function handleApi(req, res, urlObj, body) {
res.end(JSON.stringify({ ok: true, agentId, workspacePath: wsPath, configBackup: bakPath, res.end(JSON.stringify({ ok: true, agentId, workspacePath: wsPath, configBackup: bakPath,
notes: [ notes: [
'Discord sub-agent created (thread-only binding)', 'Discord sub-agent created (thread-only binding)',
'Workspace + runtime directories created', 'Workspace + runtime directories created (including memory/CURRENT_STATE.md)',
'Configuration updated and backed up', 'Configuration updated and backed up',
'Restart gateway to apply: openclaw gateway restart' 'Restart gateway to apply: openclaw gateway restart'
] ]
@@ -653,7 +701,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, telegramUserId } = body; const { agentId, displayName, groupId, workspaceFolder, model, purpose, personality, initialMemory, parentAgentId, telegramUserId, skills, toolGroups } = 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;
} }
@@ -682,6 +730,7 @@ async function handleApi(req, res, urlObj, body) {
const wsAlias= `~/.openclaw/workspaces/${folder}`; const wsAlias= `~/.openclaw/workspaces/${folder}`;
const agentEntry = { id: agentId, name: displayName || agentId, workspace: wsAlias, parentAgentId: String(parentAgentId).trim() }; const agentEntry = { id: agentId, name: displayName || agentId, workspace: wsAlias, parentAgentId: String(parentAgentId).trim() };
if (model && model !== '__default__') agentEntry.model = { primary: model }; if (model && model !== '__default__') agentEntry.model = { primary: model };
applySkillsTools(agentEntry, skills, toolGroups);
cfg.agents.list.push(agentEntry); cfg.agents.list.push(agentEntry);
// Binding uses parent's accountId // Binding uses parent's accountId
const parentAccountId = parentBinding.match.accountId; const parentAccountId = parentBinding.match.accountId;
@@ -706,6 +755,7 @@ async function handleApi(req, res, urlObj, body) {
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');
await fsp.writeFile(path.join(wsPath, 'memory', 'CURRENT_STATE.md'), generateCurrentStateMd(), 'utf8');
// Create agents/<id>/ runtime directory (required by OpenClaw gateway) // Create agents/<id>/ runtime directory (required by OpenClaw gateway)
const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId); const agentRuntimeDir = path.join(OPENCLAW_DIR, 'agents', agentId);
await fsp.mkdir(agentRuntimeDir, { recursive: true }); await fsp.mkdir(agentRuntimeDir, { recursive: true });
@@ -714,7 +764,7 @@ async function handleApi(req, res, urlObj, body) {
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 and runtime directories created', 'Workspace and runtime directories created (including memory/CURRENT_STATE.md)',
'Configuration updated and backed up', 'Configuration updated and backed up',
'Restart gateway to apply: openclaw gateway restart' 'Restart gateway to apply: openclaw gateway restart'
], ],
@@ -1699,6 +1749,19 @@ header { background:var(--surface); border-bottom:1px solid var(--border); paddi
.lang-toggle { background:var(--border); border:none; color:var(--muted); border-radius:6px; padding:5px 10px; font-size:12px; cursor:pointer; } .lang-toggle { background:var(--border); border:none; color:var(--muted); border-radius:6px; padding:5px 10px; font-size:12px; cursor:pointer; }
.lang-toggle:hover { background:#3a3f5c; color:var(--text); } .lang-toggle:hover { background:#3a3f5c; color:var(--text); }
/* skills/tools picker */
.skills-picker { background:var(--bg); border:1px solid var(--border); border-radius:10px; padding:12px 14px; margin-top:4px; }
.skills-picker summary { font-size:13px; font-weight:600; cursor:pointer; color:var(--text); user-select:none; }
.skills-picker summary:hover { color:var(--accent); }
.skills-picker .sp-grid { display:grid; grid-template-columns:1fr 1fr; gap:4px 16px; margin-top:8px; }
.skills-picker label.sp-item { display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--muted); cursor:pointer; padding:3px 0; }
.skills-picker label.sp-item:hover { color:var(--text); }
.skills-picker label.sp-item input[type=checkbox] { accent-color:var(--accent); width:14px; height:14px; }
.skills-picker .sp-actions { display:flex; gap:8px; margin-top:8px; padding-top:8px; border-top:1px solid var(--border); }
.skills-picker .sp-actions button { font-size:11px; padding:3px 10px; border-radius:6px; border:1px solid var(--border); background:var(--surface); color:var(--muted); cursor:pointer; }
.skills-picker .sp-actions button:hover { color:var(--text); border-color:var(--accent); }
.sp-section-title { font-size:11px; font-weight:700; color:var(--accent); text-transform:uppercase; letter-spacing:.5px; margin:8px 0 4px; grid-column:1/-1; }
/* centered version badge */ /* centered version badge */
.top-version{ position:absolute; left:50%; transform:translateX(-50%); font-size:12px; font-weight:800; letter-spacing:.3px; color:var(--text); background:rgba(108,99,255,.18); border:1px solid rgba(108,99,255,.45); padding:4px 12px; border-radius:999px; box-shadow:0 6px 18px rgba(0,0,0,.25); } .top-version{ position:absolute; left:50%; transform:translateX(-50%); font-size:12px; font-weight:800; letter-spacing:.3px; color:var(--text); background:rgba(108,99,255,.18); border:1px solid rgba(108,99,255,.45); padding:4px 12px; border-radius:999px; box-shadow:0 6px 18px rgba(0,0,0,.25); }
.ver-old{ display:none; } .ver-old{ display:none; }
@@ -1978,7 +2041,7 @@ const MAIN_HTML_BODY = String.raw`
</div> </div>
<div class="status-row"> <div class="status-row">
<div class="dot" id="dot"></div> <div class="dot" id="dot"></div>
<span class="status-txt" id="statusTxt">连接中...</span> <span class="status-txt" id="statusTxt"></span>
</div> </div>
<button class="lang-toggle" onclick="toggleLang()" id="langToggleBtn">EN</button> <button class="lang-toggle" onclick="toggleLang()" id="langToggleBtn">EN</button>
<div class="menu-wrap"> <div class="menu-wrap">
@@ -2029,6 +2092,16 @@ const MAIN_HTML_BODY = String.raw`
</div> </div>
</div> </div>
<div class="dash-gauges" id="dashGauges"><div class="empty">Loading...</div></div> <div class="dash-gauges" id="dashGauges"><div class="empty">Loading...</div></div>
<div class="card dash-notice">
<h3 data-i18n="dash.firstRunTitle">🚀 First-run checklist</h3>
<ul class="dash-note-list">
<li data-i18n="dash.firstRun1">Confirm OCM is pointed at the correct OpenClaw directory.</li>
<li data-i18n="dash.firstRun2">Check this Dashboard first: make sure Gateway is running and the system looks healthy.</li>
<li data-i18n="dash.firstRun3">Open Agents / Routing and confirm the bindings you expect are actually there.</li>
<li data-i18n="dash.firstRun4">Use the built-in Terminal to run <code>openclaw doctor</code> once so you catch environment/auth issues early.</li>
<li data-i18n="dash.firstRun5">Before bigger edits, make a backup or check rollback so recovery is nearby if something goes wrong.</li>
</ul>
</div>
<div class="dash-sections"> <div class="dash-sections">
<div class="card dash-card" id="dashSystem"> <div class="card dash-card" id="dashSystem">
<h3>🖥 System Info</h3> <h3>🖥 System Info</h3>
@@ -2174,6 +2247,7 @@ const MAIN_HTML_BODY = String.raw`
<select id="cliPreset" onchange="onCliPresetSelect()" style="font-size:11px;padding:3px 6px;max-width:180px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text)"> <select id="cliPreset" onchange="onCliPresetSelect()" style="font-size:11px;padding:3px 6px;max-width:180px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text)">
<option value="" data-i18n="cli.presets"> 常用命令 </option> <option value="" data-i18n="cli.presets"> 常用命令 </option>
</select> </select>
<button class="btn-secondary" style="font-size:11px;padding:3px 10px" onclick="copyCliCommand()" data-i18n="cli.copy">复制命令</button>
<button class="btn-secondary" style="font-size:11px;padding:3px 10px" onclick="clearCliOutput()" data-i18n="cli.clear">清空</button> <button class="btn-secondary" style="font-size:11px;padding:3px 10px" onclick="clearCliOutput()" data-i18n="cli.clear">清空</button>
<button class="btn-secondary" style="font-size:11px;padding:3px 10px" onclick="toggleCliPanel()" data-i18n="cli.collapse"> 收起</button> <button class="btn-secondary" style="font-size:11px;padding:3px 10px" onclick="toggleCliPanel()" data-i18n="cli.collapse"> 收起</button>
</div> </div>
@@ -2438,6 +2512,12 @@ const I18N = {
'models.onlyCliHint':'模型下拉仅显示 openclaw models list 返回的模型。', 'models.onlyCliHint':'模型下拉仅显示 openclaw models list 返回的模型。',
'models.modelListErr':'读取 openclaw models list 失败:', 'models.modelListErr':'读取 openclaw models list 失败:',
'models.modelListEmpty':'未从 openclaw models list 解析到模型。请先运行 openclaw onboard 并确认模型可用。', 'models.modelListEmpty':'未从 openclaw models list 解析到模型。请先运行 openclaw onboard 并确认模型可用。',
'dash.firstRunTitle':'🚀 首次使用检查清单',
'dash.firstRun1':'先确认 OCM 指向的是正确的 OpenClaw 数据目录。',
'dash.firstRun2':'先看 Dashboard:确认 Gateway 正在运行,系统状态看起来正常。',
'dash.firstRun3':'打开 Agents / Routing,确认你期望的绑定确实已经存在。',
'dash.firstRun4':'用内置终端先运行一次 <code>openclaw doctor</code>,尽早发现环境或认证问题。',
'dash.firstRun5':'在做较大改动前,先做一次备份或确认回滚入口,出问题时更容易恢复。',
'dash.noticeTitle':'📌 Telegram 使用说明(重要)', 'dash.noticeTitle':'📌 Telegram 使用说明(重要)',
'dash.notice1':'OCM 主要面向 Telegram:通过群组绑定 Agent,让每个 Agent 拥有独立 Workspace / SOUL.md / MEMORY.md。', 'dash.notice1':'OCM 主要面向 Telegram:通过群组绑定 Agent,让每个 Agent 拥有独立 Workspace / SOUL.md / MEMORY.md。',
'dash.notice2':'请确保你已有基本 OpenClaw 操作经验。OCM 主要负责可视化更新 openclaw.json,方便增删主 Agent 与 Sub-Agent,并支持多条 Agent 树。', 'dash.notice2':'请确保你已有基本 OpenClaw 操作经验。OCM 主要负责可视化更新 openclaw.json,方便增删主 Agent 与 Sub-Agent,并支持多条 Agent 树。',
@@ -2469,7 +2549,7 @@ const I18N = {
'actions.restoreConfirm':'确认将配置恢复为:\\n{filename}\\n\\n当前配置将先被自动备份。继续?', 'actions.restoreConfirm':'确认将配置恢复为:\\n{filename}\\n\\n当前配置将先被自动备份。继续?',
'actions.restored':'已恢复 ','actions.restoreFail':'恢复失败: ', 'actions.restored':'已恢复 ','actions.restoreFail':'恢复失败: ',
'actions.setupEmpty':'请填写路径','actions.setupSwitching':'目录已切换,正在刷新...','actions.setupInvalid':'路径无效','actions.setupReqFail':'请求失败: ', 'actions.setupEmpty':'请填写路径','actions.setupSwitching':'目录已切换,正在刷新...','actions.setupInvalid':'路径无效','actions.setupReqFail':'请求失败: ',
'common.loading':'加载中...','common.close':'关闭', 'common.loading':'加载中...','common.close':'关闭','status.connecting':'连接中...','status.configReadError':'无法读取配置',
'btn.save':'保存','btn.delete':'删除','btn.cancel':'取消','btn.add':'添加','btn.remove':'移除', 'btn.save':'保存','btn.delete':'删除','btn.cancel':'取消','btn.add':'添加','btn.remove':'移除',
'agents.addAgent':' Add Agent','agents.addSub':' Add Sub-Agent','agents.hint':'Add Agent: create a top-level agent (a long-lived role) for a new domain. Add Sub-Agent: create a child under a parent; sub-agents use the parent Telegram bot but have their own workspace + SOUL.md + MEMORY.md, preventing context mixing, improving focus, and saving tokens.', 'agents.addAgent':' Add Agent','agents.addSub':' Add Sub-Agent','agents.hint':'Add Agent: create a top-level agent (a long-lived role) for a new domain. Add Sub-Agent: create a child under a parent; sub-agents use the parent Telegram bot but have their own workspace + SOUL.md + MEMORY.md, preventing context mixing, improving focus, and saving tokens.',
'agents.addAgentTitle':'Add Agent (Main Bot)','agents.addSubTitle':'Add Sub-Agent', 'agents.addAgentTitle':'Add Agent (Main Bot)','agents.addSubTitle':'Add Sub-Agent',
@@ -2496,6 +2576,11 @@ const I18N = {
'guide.sub.discord.s3':'右键该 Thread → <b>Copy Link</b>,粘贴到表单里(OCM 自动解析 <code>threadId</code>)。', 'guide.sub.discord.s3':'右键该 Thread → <b>Copy Link</b>,粘贴到表单里(OCM 自动解析 <code>threadId</code>)。',
'guide.sub.discord.warn':'⚠️ 约定:主 Agent 绑定 ChannelSub-Agent 绑定 Thread。不要把 Sub-Agent 绑到普通 Channel。', 'guide.sub.discord.warn':'⚠️ 约定:主 Agent 绑定 ChannelSub-Agent 绑定 Thread。不要把 Sub-Agent 绑到普通 Channel。',
'wiz.telegramId':'你的 Telegram User ID','wiz.telegramIdHint':'💡 可通过 @userinfobot 获取,填写后自动配置 allowFrom 白名单','wiz.telegramIdPh':'例如: 123456789', 'wiz.telegramId':'你的 Telegram User ID','wiz.telegramIdHint':'💡 可通过 @userinfobot 获取,填写后自动配置 allowFrom 白名单','wiz.telegramIdPh':'例如: 123456789',
'wiz.channel':'渠道',
'wiz.discordChannel':'Discord 频道链接/ID',
'wiz.discordChannelHint':'建议:右键频道 → Copy Link,粘贴后自动解析 channelId(可选 guildId)。',
'wiz.discordThread':'Discord Thread 链接/ID(仅 Thread',
'wiz.discordThreadHint':'建议:在主频道内创建 thread → 右键 thread → Copy Link,粘贴后自动解析 threadId。',
'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':'默认',
@@ -2515,6 +2600,9 @@ const I18N = {
'wiz.soul':'性格关键词','wiz.soulHint':'(逗号分隔,选填)','wiz.soulPh':'幽默、直接、有条理...', 'wiz.soul':'性格关键词','wiz.soulHint':'(逗号分隔,选填)','wiz.soulPh':'幽默、直接、有条理...',
'wiz.soulTip':'留空则使用默认成长型提示词(推荐)', 'wiz.soulTip':'留空则使用默认成长型提示词(推荐)',
'wiz.memory':'初始记忆','wiz.memoryHint':'MEMORY.md,选填)','wiz.memoryPh':'例如:群组主要用中文交流。用户偏好简洁回复。', 'wiz.memory':'初始记忆','wiz.memoryHint':'MEMORY.md,选填)','wiz.memoryPh':'例如:群组主要用中文交流。用户偏好简洁回复。',
'wiz.skillsTools':'Skills & Tools 配置','wiz.skillsHint':'不选择则使用默认预设(推荐)',
'wiz.skillsSection':'Skills(能力)','wiz.toolsSection':'Tool Groups(权限)',
'wiz.spSelectDefault':'仅默认','wiz.spSelectAll':'全选','wiz.spSelectNone':'清空',
'wiz.preview':'即将创建:','wiz.group':'群组','wiz.modelLabel':'模型','wiz.globalDefault':'全局默认', 'wiz.preview':'即将创建:','wiz.group':'群组','wiz.modelLabel':'模型','wiz.globalDefault':'全局默认',
'wiz.soulYes':'含性格关键词','wiz.soulDefault':'默认成长型提示词(推荐)', 'wiz.soulYes':'含性格关键词','wiz.soulDefault':'默认成长型提示词(推荐)',
'wiz.autoBackup':'自动备份当前 openclaw.json ✓', 'wiz.autoBackup':'自动备份当前 openclaw.json ✓',
@@ -2528,7 +2616,7 @@ const I18N = {
'ch.peerId':'Peer ID','ch.peerIdHint':'(群组 ID 或用户 ID','ch.peerIdTip':'留空则匹配所有对应类型的 Peer', 'ch.peerId':'Peer ID','ch.peerIdHint':'(群组 ID 或用户 ID','ch.peerIdTip':'留空则匹配所有对应类型的 Peer',
'ch.submit':'添加绑定', 'ch.submit':'添加绑定',
'l.sub':'选择运行模式', 'l.sub':'选择运行模式',
'cli.open':'⌨️ 终端','cli.title':'⌨️ CLI 终端','cli.clear':'清空','cli.collapse':'▼ 收起', 'cli.open':'⌨️ 终端','cli.title':'⌨️ CLI 终端','cli.copy':'复制命令','cli.clear':'清空','cli.collapse':'▼ 收起',
'cli.ready':'── 终端就绪,等待命令 ──','cli.cleared':'── 已清空 ──', 'cli.ready':'── 终端就绪,等待命令 ──','cli.cleared':'── 已清空 ──',
'cli.presets':'── 常用命令 ──','cli.builtins':'内置命令','cli.favs':'我的收藏', 'cli.presets':'── 常用命令 ──','cli.builtins':'内置命令','cli.favs':'我的收藏',
'cli.run':'▶ 执行','cli.stop':'■ 停止','cli.star':'⭐','cli.manage':'管理', 'cli.run':'▶ 执行','cli.stop':'■ 停止','cli.star':'⭐','cli.manage':'管理',
@@ -2567,6 +2655,12 @@ const I18N = {
'models.onlyCliHint':'Model dropdowns only show IDs returned by openclaw models list.', 'models.onlyCliHint':'Model dropdowns only show IDs returned by openclaw models list.',
'models.modelListErr':'Failed to load 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.', 'models.modelListEmpty':'No model IDs were parsed from openclaw models list. Run openclaw onboard first.',
'dash.firstRunTitle':'🚀 First-run checklist',
'dash.firstRun1':'Confirm OCM is pointed at the correct OpenClaw directory.',
'dash.firstRun2':'Check Dashboard first: make sure Gateway is running and the system looks healthy.',
'dash.firstRun3':'Open Agents / Routing and confirm the bindings you expect are actually there.',
'dash.firstRun4':'Use the built-in Terminal to run <code>openclaw doctor</code> once so you catch environment or auth issues early.',
'dash.firstRun5':'Before bigger edits, make a backup or check rollback so recovery is nearby if something goes wrong.',
'dash.noticeTitle':'📌 Telegram Usage Notes (Important)', '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.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.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.',
@@ -2598,7 +2692,7 @@ const I18N = {
'actions.restoreConfirm':'Restore config to:\\n{filename}\\n\\nCurrent config will be auto-backed up first. Continue?', 'actions.restoreConfirm':'Restore config to:\\n{filename}\\n\\nCurrent config will be auto-backed up first. Continue?',
'actions.restored':'Restored ','actions.restoreFail':'Restore failed: ', 'actions.restored':'Restored ','actions.restoreFail':'Restore failed: ',
'actions.setupEmpty':'Please enter a path','actions.setupSwitching':'Directory switched, refreshing...','actions.setupInvalid':'Invalid path','actions.setupReqFail':'Request failed: ', 'actions.setupEmpty':'Please enter a path','actions.setupSwitching':'Directory switched, refreshing...','actions.setupInvalid':'Invalid path','actions.setupReqFail':'Request failed: ',
'common.loading':'Loading...','common.close':'Close', 'common.loading':'Loading...','common.close':'Close','status.connecting':'Connecting...','status.configReadError':'Unable to read config',
'btn.save':'Save','btn.delete':'Delete','btn.cancel':'Cancel','btn.add':'Add','btn.remove':'Remove', 'btn.save':'Save','btn.delete':'Delete','btn.cancel':'Cancel','btn.add':'Add','btn.remove':'Remove',
'agents.addAgent':' Add Agent','agents.addSub':' Add Sub-Agent','agents.hint':'Add Agent: create a top-level agent (a long-lived role) for a new domain. Add Sub-Agent: create a child under a parent; sub-agents use the parent Telegram bot but have their own workspace + SOUL.md + MEMORY.md, preventing context mixing, improving focus, and saving tokens.', 'agents.addAgent':' Add Agent','agents.addSub':' Add Sub-Agent','agents.hint':'Add Agent: create a top-level agent (a long-lived role) for a new domain. Add Sub-Agent: create a child under a parent; sub-agents use the parent Telegram bot but have their own workspace + SOUL.md + MEMORY.md, preventing context mixing, improving focus, and saving tokens.',
'agents.addAgentTitle':'Add Agent (Main Bot)','agents.addSubTitle':'Add Sub-Agent', 'agents.addAgentTitle':'Add Agent (Main Bot)','agents.addSubTitle':'Add Sub-Agent',
@@ -2625,6 +2719,12 @@ const I18N = {
'guide.sub.discord.s3':'Right-click the thread → <b>Copy Link</b>, paste into the form (OCM auto-parses the <code>threadId</code>).', 'guide.sub.discord.s3':'Right-click the thread → <b>Copy Link</b>, paste into the form (OCM auto-parses the <code>threadId</code>).',
'guide.sub.discord.warn':'Important: main agents bind to channels; sub-agents bind to threads. Do not bind sub-agents to normal channels.', 'guide.sub.discord.warn':'Important: main agents bind to channels; sub-agents bind to threads. Do not bind sub-agents to normal channels.',
'wiz.telegramId':'Your Telegram User ID','wiz.telegramIdHint':'💡 Get it from @userinfobot — auto-configures allowFrom whitelist','wiz.telegramIdPh':'e.g. 123456789', 'wiz.telegramId':'Your Telegram User ID','wiz.telegramIdHint':'💡 Get it from @userinfobot — auto-configures allowFrom whitelist','wiz.telegramIdPh':'e.g. 123456789',
'wiz.channel':'Channel',
'wiz.discordChannel':'Discord channel link/ID',
'wiz.discordChannelHint':'Tip: right-click channel → Copy Link. Paste to auto-parse channelId (optional guildId).',
'wiz.discordThread':'Discord thread link/ID (thread only)',
'wiz.discordThreadHint':'Tip: create a thread under your main channel → Copy Link. Paste to auto-parse threadId.',
'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',
@@ -2644,6 +2744,9 @@ const I18N = {
'wiz.soul':'Personality Keywords','wiz.soulHint':'(comma-separated, optional)','wiz.soulPh':'humorous, direct, organized...', 'wiz.soul':'Personality Keywords','wiz.soulHint':'(comma-separated, optional)','wiz.soulPh':'humorous, direct, organized...',
'wiz.soulTip':'Leave blank for default growth prompt (recommended)', 'wiz.soulTip':'Leave blank for default growth prompt (recommended)',
'wiz.memory':'Initial Memory','wiz.memoryHint':'(MEMORY.md, optional)','wiz.memoryPh':'e.g. Group mainly uses English. Users prefer concise replies.', 'wiz.memory':'Initial Memory','wiz.memoryHint':'(MEMORY.md, optional)','wiz.memoryPh':'e.g. Group mainly uses English. Users prefer concise replies.',
'wiz.skillsTools':'Skills & Tools','wiz.skillsHint':'Leave as-is for defaults (recommended)',
'wiz.skillsSection':'Skills','wiz.toolsSection':'Tool Groups',
'wiz.spSelectDefault':'Default','wiz.spSelectAll':'All','wiz.spSelectNone':'None',
'wiz.preview':'About to create:','wiz.group':'Group','wiz.modelLabel':'Model','wiz.globalDefault':'Global Default', 'wiz.preview':'About to create:','wiz.group':'Group','wiz.modelLabel':'Model','wiz.globalDefault':'Global Default',
'wiz.soulYes':'With personality keywords','wiz.soulDefault':'Default growth prompt (recommended)', 'wiz.soulYes':'With personality keywords','wiz.soulDefault':'Default growth prompt (recommended)',
'wiz.autoBackup':'Auto-backup current openclaw.json ✓', 'wiz.autoBackup':'Auto-backup current openclaw.json ✓',
@@ -2657,7 +2760,7 @@ const I18N = {
'ch.peerId':'Peer ID','ch.peerIdHint':'(Group ID or User ID)','ch.peerIdTip':'Leave blank to match all peers of this type', 'ch.peerId':'Peer ID','ch.peerIdHint':'(Group ID or User ID)','ch.peerIdTip':'Leave blank to match all peers of this type',
'ch.submit':'Add Binding', 'ch.submit':'Add Binding',
'l.sub':'Select Mode', 'l.sub':'Select Mode',
'cli.open':'⌨️ Terminal','cli.title':'⌨️ CLI Terminal','cli.clear':'Clear','cli.collapse':'▼ Collapse', 'cli.open':'⌨️ Terminal','cli.title':'⌨️ CLI Terminal','cli.copy':'Copy cmd','cli.clear':'Clear','cli.collapse':'▼ Collapse',
'cli.ready':'── Terminal Ready ──','cli.cleared':'── Cleared ──', 'cli.ready':'── Terminal Ready ──','cli.cleared':'── Cleared ──',
'cli.presets':'── Presets ──','cli.builtins':'Built-in','cli.favs':'My Favorites', 'cli.presets':'── Presets ──','cli.builtins':'Built-in','cli.favs':'My Favorites',
'cli.run':'▶ Run','cli.stop':'■ Stop','cli.star':'⭐','cli.manage':'Manage', 'cli.run':'▶ Run','cli.stop':'■ Stop','cli.star':'⭐','cli.manage':'Manage',
@@ -2695,6 +2798,9 @@ const LS = {
let lang = LS.get('ocm_lang', 'en'); 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.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
const st = document.getElementById('statusTxt');
if(st && (!st.textContent || st.textContent === '连接中...' || st.textContent === 'Connecting...')) st.textContent = t('status.connecting');
document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); }); document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); });
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { el.placeholder = t(el.dataset.i18nPlaceholder); }); document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { el.placeholder = t(el.dataset.i18nPlaceholder); });
const ltb = document.getElementById('langToggleBtn'); const ltb = document.getElementById('langToggleBtn');
@@ -2763,7 +2869,7 @@ async function checkStatus(){
if(tv) tv.textContent = 'OCM v'+(r.ocmVersion||'--'); if(tv) tv.textContent = 'OCM v'+(r.ocmVersion||'--');
const vb=document.getElementById('versionBadge'); if(vb) vb.textContent = 'v'+r.version; const vb=document.getElementById('versionBadge'); if(vb) vb.textContent = 'v'+r.version;
const ov=document.getElementById('ocmVersionBadge'); if(ov) ov.textContent='ocm v'+(r.ocmVersion||'--'); const ov=document.getElementById('ocmVersionBadge'); if(ov) ov.textContent='ocm v'+(r.ocmVersion||'--');
}catch{ setDot('err'); document.getElementById('statusTxt').textContent='无法读取配置'; } }catch{ setDot('err'); document.getElementById('statusTxt').textContent=t('status.configReadError'); }
} }
async function loadAll(){ async function loadAll(){
@@ -2920,6 +3026,78 @@ function showAddForm(type) {
applyLang(); applyLang();
} }
// ── Skills / Tools picker helper ──────────────────────────
const AVAILABLE_SKILLS = [
{ id:'memory-continuity', label:'Memory Continuity', isDefault:true },
{ id:'agent-workflow', label:'Agent Workflow', isDefault:true },
{ id:'execution-agent-dispatch', label:'Execution Dispatch', isDefault:true },
{ id:'session-logs', label:'Session Logs', isDefault:true },
{ id:'browser-use', label:'Browser Use', isDefault:false },
{ id:'github', label:'GitHub', isDefault:false },
{ id:'gh-issues', label:'GitHub Issues', isDefault:false },
{ id:'coding-agent', label:'Coding Agent', isDefault:false },
{ id:'execution-agent-planner', label:'Execution Planner', isDefault:false },
{ id:'discord', label:'Discord', isDefault:false },
{ id:'weather', label:'Weather', isDefault:false },
{ id:'summarize', label:'Summarize', isDefault:false },
{ id:'healthcheck', label:'Healthcheck', isDefault:false },
];
const AVAILABLE_TOOL_GROUPS = [
{ id:'group:fs', label:'Filesystem (group:fs)', isDefault:true },
{ id:'group:runtime', label:'Runtime (group:runtime)', isDefault:true },
{ id:'group:memory', label:'Memory (group:memory)', isDefault:true },
{ id:'sessions_spawn', label:'Sessions Spawn', isDefault:true },
{ id:'subagents', label:'Subagents', isDefault:true },
];
function buildSkillsToolsPicker(prefix) {
let html = '<details class="skills-picker" id="'+prefix+'-sp-wrap"><summary>' + t('wiz.skillsTools') + ' <span style="color:var(--muted);font-weight:400;font-size:12px">' + t('wiz.skillsHint') + '</span></summary>';
html += '<div class="sp-section-title">' + t('wiz.skillsSection') + '</div>';
html += '<div class="sp-grid">';
AVAILABLE_SKILLS.forEach(s => {
html += '<label class="sp-item"><input type="checkbox" data-sp-type="skill" value="'+s.id+'" '+(s.isDefault?'checked':'')+'>'+s.label+'</label>';
});
html += '</div>';
html += '<div class="sp-section-title">' + t('wiz.toolsSection') + '</div>';
html += '<div class="sp-grid">';
AVAILABLE_TOOL_GROUPS.forEach(g => {
html += '<label class="sp-item"><input type="checkbox" data-sp-type="tool" value="'+g.id+'" '+(g.isDefault?'checked':'')+'>'+g.label+'</label>';
});
html += '</div>';
html += '<div class="sp-actions">';
html += '<button type="button" onclick="spAction(\\x27'+prefix+'\\x27,\\x27default\\x27)">' + t('wiz.spSelectDefault') + '</button>';
html += '<button type="button" onclick="spAction(\\x27'+prefix+'\\x27,\\x27all\\x27)">' + t('wiz.spSelectAll') + '</button>';
html += '<button type="button" onclick="spAction(\\x27'+prefix+'\\x27,\\x27none\\x27)">' + t('wiz.spSelectNone') + '</button>';
html += '</div></details>';
return html;
}
function spAction(prefix, action) {
const wrap = document.getElementById(prefix+'-sp-wrap');
if (!wrap) return;
const boxes = wrap.querySelectorAll('input[type=checkbox]');
if (action==='none') { boxes.forEach(b=>b.checked=false); return; }
if (action==='all') { boxes.forEach(b=>b.checked=true); return; }
// default
const defSkills = AVAILABLE_SKILLS.filter(s=>s.isDefault).map(s=>s.id);
const defTools = AVAILABLE_TOOL_GROUPS.filter(g=>g.isDefault).map(g=>g.id);
boxes.forEach(b=>{
if (b.dataset.spType==='skill') b.checked = defSkills.includes(b.value);
else b.checked = defTools.includes(b.value);
});
}
function collectSkillsTools(prefix) {
const wrap = document.getElementById(prefix+'-sp-wrap');
if (!wrap) return { skills:[], toolGroups:[] };
const skills = []; const toolGroups = [];
wrap.querySelectorAll('input[type=checkbox]:checked').forEach(b=>{
if (b.dataset.spType==='skill') skills.push(b.value);
else toolGroups.push(b.value);
});
return { skills, toolGroups };
}
function buildAddAgentForm() { function buildAddAgentForm() {
const modelOpts = buildModelOpts('__default__'); const modelOpts = buildModelOpts('__default__');
return '<div class="add-form">' + return '<div class="add-form">' +
@@ -2977,6 +3155,7 @@ function buildAddAgentForm() {
'<div class="form-group"><label>' + t('wiz.soul') + ' <span style="color:var(--muted);font-weight:400">' + t('wiz.soulHint') + '</span></label>' + '<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') + '">' + '<input id="fa-soul" placeholder="' + t('wiz.soulPh') + '">' +
'<span class="hint-text">' + t('wiz.soulTip') + '</span></div>' + '<span class="hint-text">' + t('wiz.soulTip') + '</span></div>' +
'<div class="form-group">' + buildSkillsToolsPicker('fa') + '</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>' +
@@ -3072,6 +3251,7 @@ function buildAddSubForm() {
'<input id="fs-soul" placeholder="' + t('wiz.soulPh') + '"></div>' + '<input id="fs-soul" placeholder="' + t('wiz.soulPh') + '"></div>' +
'<div class="form-group"><label>' + t('wiz.memory') + ' <span style="color:var(--muted);font-weight:400">' + t('wiz.memoryHint') + '</span></label>' + '<div class="form-group"><label>' + t('wiz.memory') + ' <span style="color:var(--muted);font-weight:400">' + t('wiz.memoryHint') + '</span></label>' +
'<textarea id="fs-mem" placeholder="' + t('wiz.memoryPh') + '" rows="2"></textarea></div>' + '<textarea id="fs-mem" placeholder="' + t('wiz.memoryPh') + '" rows="2"></textarea></div>' +
'<div class="form-group">' + buildSkillsToolsPicker('fs') + '</div>' +
'<div style="display:flex;gap:8px;margin-top:14px">' + '<div style="display:flex;gap:8px;margin-top:14px">' +
'<button class="btn-primary" onclick="submitAddSub()">' + t('agents.addSubSubmit') + '</button>' + '<button class="btn-primary" onclick="submitAddSub()">' + t('agents.addSubSubmit') + '</button>' +
@@ -3105,18 +3285,20 @@ async function submitAddAgent() {
if (!name) { toast(t('agents.errName'), 'err'); return; } if (!name) { toast(t('agents.errName'), 'err'); return; }
if (!workspace) { toast('Workspace folder is required', 'err'); return; } if (!workspace) { toast('Workspace folder is required', 'err'); return; }
const { skills, toolGroups } = collectSkillsTools('fa');
try { try {
if (channel === 'telegram') { if (channel === 'telegram') {
const token = document.getElementById('fa-token').value.trim(); const token = document.getElementById('fa-token').value.trim();
if (!token) { toast(t('agents.errToken'), 'err'); return; } if (!token) { toast(t('agents.errToken'), 'err'); return; }
const payload = { botToken: token, agentId, name, workspace, model: model === '__default__' ? '' : model, purpose, personality }; const payload = { botToken: token, agentId, name, workspace, model: model === '__default__' ? '' : model, purpose, personality, skills, toolGroups };
await api('POST', '/api/agents/bot', payload); await api('POST', '/api/agents/bot', payload);
} else { } else {
const link = (document.getElementById('fa-discord-link')?.value||'').trim(); const link = (document.getElementById('fa-discord-link')?.value||'').trim();
const parsed = parseDiscordLinkOrId(link); const parsed = parseDiscordLinkOrId(link);
if (!parsed) { toast('Discord channel link/ID is required', 'err'); return; } if (!parsed) { toast('Discord channel link/ID is required', 'err'); return; }
const payload = { agentId, name, workspaceFolder: workspace, model: model === '__default__' ? '' : model, purpose, personality, const payload = { agentId, name, workspaceFolder: workspace, model: model === '__default__' ? '' : model, purpose, personality,
guildId: parsed.guildId, channelId: parsed.channelId }; guildId: parsed.guildId, channelId: parsed.channelId, skills, toolGroups };
await api('POST', '/api/agents/discord', payload); await api('POST', '/api/agents/discord', payload);
} }
toast('Agent created successfully', 'ok'); toast('Agent created successfully', 'ok');
@@ -3145,6 +3327,8 @@ async function submitAddSub() {
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 (!workspaceFolder) { toast('Workspace folder is required', 'err'); return; } if (!workspaceFolder) { toast('Workspace folder is required', 'err'); return; }
const { skills, toolGroups } = collectSkillsTools('fs');
try { try {
if (channel === 'telegram') { if (channel === 'telegram') {
const groupId = document.getElementById('fs-gid').value.trim(); const groupId = document.getElementById('fs-gid').value.trim();
@@ -3152,14 +3336,14 @@ async function submitAddSub() {
if (!groupId) { toast(t('wiz.errGroupId'), 'err'); return; } if (!groupId) { toast(t('wiz.errGroupId'), 'err'); return; }
if (telegramUserId && !/^\d+$/.test(telegramUserId)) { toast('Telegram User ID must be a number', 'err'); return; } if (telegramUserId && !/^\d+$/.test(telegramUserId)) { toast('Telegram User ID must be a number', 'err'); return; }
await api('POST', '/api/agents', { parentAgentId, agentId, displayName: displayName || agentId, groupId, await api('POST', '/api/agents', { parentAgentId, agentId, displayName: displayName || agentId, groupId,
workspaceFolder, model: model === '__default__' ? '' : model, purpose, personality, initialMemory, telegramUserId }); workspaceFolder, model: model === '__default__' ? '' : model, purpose, personality, initialMemory, telegramUserId, skills, toolGroups });
} else { } else {
const link = (document.getElementById('fs-discord-link')?.value||'').trim(); const link = (document.getElementById('fs-discord-link')?.value||'').trim();
const parsed = parseDiscordLinkOrId(link); const parsed = parseDiscordLinkOrId(link);
if (!parsed) { toast('Discord thread link/ID is required', 'err'); return; } if (!parsed) { toast('Discord thread link/ID is required', 'err'); return; }
await api('POST', '/api/agents/discord-sub', { parentAgentId, agentId, displayName: displayName || agentId, await api('POST', '/api/agents/discord-sub', { parentAgentId, agentId, displayName: displayName || agentId,
workspaceFolder, model: model === '__default__' ? '' : model, purpose, personality, initialMemory, workspaceFolder, model: model === '__default__' ? '' : model, purpose, personality, initialMemory,
guildId: parsed.guildId, threadId: parsed.channelId }); guildId: parsed.guildId, threadId: parsed.channelId, skills, toolGroups });
} }
toast(t('wiz.created'), 'ok'); toast(t('wiz.created'), 'ok');
clearAddForm(); clearAddForm();
@@ -3185,7 +3369,7 @@ function renderAgents() {
} }
}); });
const roots=[]; const childrenByRoot={}; const orphans=[]; const roots=[]; const childrenByRoot={};
(S.agents||[]).forEach(a=>{ (S.agents||[]).forEach(a=>{
const pid = a.parentAgentId || a._inferParentAgentId || ''; const pid = a.parentAgentId || a._inferParentAgentId || '';
if(pid && byId[pid]){ if(pid && byId[pid]){
@@ -3194,6 +3378,12 @@ function renderAgents() {
roots.push(a); roots.push(a);
} }
}); });
// Sort roots: agents with own bot first (main always first among those), then orphans
roots.sort((a,b)=>{
if(a.id==='main') return -1; if(b.id==='main') return 1;
if(a.hasOwnBot && !b.hasOwnBot) return -1; if(!a.hasOwnBot && b.hasOwnBot) return 1;
return (a.name||a.id).localeCompare(b.name||b.id);
});
function fmtBinding(b){ function fmtBinding(b){
const ch = (b.channel||'').toLowerCase(); const ch = (b.channel||'').toLowerCase();
@@ -3653,7 +3843,14 @@ async function deleteAuth(key){
} }
function copyText(txt){ function copyText(txt){
navigator.clipboard.writeText(txt).then(()=>toast('已复制','success')).catch(()=>toast('复制失败','error')); navigator.clipboard.writeText(txt).then(()=>toast(lang==='en'?'Copied':'已复制','success')).catch(()=>toast(lang==='en'?'Copy failed':'复制失败','error'));
}
function copyCliCommand(){
const inp=document.getElementById('cliInput');
const cmd=(inp&&inp.value?inp.value:'').trim();
if(!cmd){ toast(lang==='en'?'Nothing to copy (CLI input is empty)':'没有可复制的命令(输入框为空)','error'); return; }
copyText(cmd);
} }
@@ -3893,8 +4090,8 @@ function runCli(){
es.addEventListener('done',e=>{ es.addEventListener('done',e=>{
try{ try{
const d=JSON.parse(e.data); const d=JSON.parse(e.data);
if(d.code===0) cliAppend('\\n✅ 完成 (exit 0)\\n','cli-done-ok'); if(d.code===0) cliAppend('\\n✅ Done (exit 0)\\n','cli-done-ok');
else cliAppend('\\n❌ 退出码 '+d.code+'\\n','cli-done-err'); else cliAppend('\\n❌ Exit code '+d.code+'\\n','cli-done-err');
}catch(_){} }catch(_){}
es.close(); cliEvt=null; setCliRunning(false); es.close(); cliEvt=null; setCliRunning(false);
}); });
@@ -4361,7 +4558,7 @@ document.querySelectorAll('.tab').forEach(t=>{
(function() { (function() {
LS.del('ocm_mode'); LS.del('ocm_mode');
applyLang(); applyLang();
checkStatus().then(() => { loadAll(); loadDashboard(); }).catch(e => console.error('Init error:', e)); checkStatus().then(() => { loadAll(); setTimeout(() => loadDashboard(), 0); }).catch(e => console.error('Init error:', e));
startHealthPolling(); startHealthPolling();
})();`; })();`;
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "openclaw-manager", "name": "openclaw-manager",
"version": "0.8.2", "version": "0.9.3",
"description": "A local web UI for managing OpenClaw AI agents no npm install required", "description": "A local web UI for managing OpenClaw AI agents \u2014 no npm install required",
"main": "openclaw-manager.js", "main": "openclaw-manager.js",
"scripts": { "scripts": {
"start": "node openclaw-manager.js", "start": "node openclaw-manager.js",