5 Commits
Author SHA1 Message Date
3d58bfc8af release: v1.0.0-rc1 — feature freeze for stable release
* fix: orphan agents now grouped under main in agent tree

Agents without parentAgentId, own bot, or binding-based inference
(e.g. dynamically created sub-agents like codex-worker) were
incorrectly displayed as top-level roots. They now default to
main's subtree.

Bump version to 0.9.4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* cleanup: drop archived workflow skill defaults

* release: v1.0.0-rc1 — feature freeze for stable release

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-04-05 07:51:39 +10:00
d3fdbcef6a fix: improve agent tree ordering and parent inference (#15)
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: Tao <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 20:33:26 +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
7 changed files with 191 additions and 19 deletions
+27 -2
View File
@@ -1,10 +1,35 @@
# OpenClaw Manager — 开发日志
> 最后更新:2026-03-01
> 当前版本:v0.9.0
> 最后更新:2026-03-17
> 当前版本: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
+8 -1
View File
@@ -156,7 +156,7 @@ That is why OCM should be thought of as a **control panel for an existing OpenCl
## 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.
- **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.
- **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.
@@ -166,6 +166,7 @@ That is why OCM should be thought of as a **control panel for an existing OpenCl
## What's New (v0.9.x)
- **Skills & Tools auto-config**: new agents/sub-agents automatically get default Skills (`memory-continuity`, `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).
- **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.
@@ -279,6 +280,12 @@ ocm/
- **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
## Related Projects
- [clawkeeper](https://github.com/dtzp555-max/clawkeeper) — OpenClaw memory operations kit for doctor / provider switch / backup / restore workflows. *(archived)*
- [execution-agent-planner](https://github.com/dtzp555-max/execution-agent-planner) — archived planning 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
MIT
+5
View File
@@ -2,6 +2,11 @@
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
- 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.
+3 -1
View File
@@ -138,8 +138,9 @@ Click `+ Add Agent` and fill:
- workspace path (recommended: separate folder per agent)
- model (dropdown is sourced from `openclaw models list`)
- the BotFather token
- **Skills & Tools** (optional): expand the collapsible picker at the bottom to customize. By default, new agents get: `memory-continuity`, `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`.
---
@@ -176,6 +177,7 @@ Send a message in the new group. In logs you should see something like:
- paste the Group ID
- set workspace (recommended: its own folder)
- choose model
- **Skills & Tools** (optional): same picker as Add Agent — defaults are pre-selected, expand to customize
**Step 4 — Allowlist (optional but recommended)**
+3 -1
View File
@@ -123,8 +123,9 @@ bash start.sh
- workspace 路径(建议独立目录)
- 选择模型(下拉来自 `openclaw models list`,更不容易选错)
- 填入刚拿到的 bot token
- **Skills & Tools**(可选):展开表单底部的折叠面板可自定义。默认预设:Skills 包含 `memory-continuity``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`
---
@@ -159,6 +160,7 @@ openclaw gateway logs --follow
- 填 Group ID
- workspace(建议单独目录)
- 选择模型
- **Skills & Tools**(可选):和 Add Agent 相同的选择器,默认预设已勾选,展开可自定义
#### Step 4:写 allowlist(推荐)
如果你启用了 Telegram 的 allowlist(例如 `channels.telegram.allowFrom`),
+144 -13
View File
@@ -24,7 +24,7 @@ const SCRIPT_DIR = __dirname;
const MANAGER_CONFIG = path.join(SCRIPT_DIR, 'manager-config.json');
let PORT = 3333;
let HOST = '0.0.0.0';
const APP_VERSION = '0.9.3';
const APP_VERSION = '1.0.0-rc1';
// --port 参数
const portIdx = process.argv.indexOf('--port');
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');
}
// ── Default Skills & Tool Groups for new agents ─────────────
const DEFAULT_SKILLS = ['memory-continuity', '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 = [
{ id: '__default__', label: '使用全局默认模型' },
@@ -417,6 +428,12 @@ async function handleApi(req, res, urlObj, body) {
);
// 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;
// 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 binding = bindings.find(b => b.agentId === a.id && b.match?.peer?.kind === 'group');
const groupId = binding?.match?.peer?.id || null;
@@ -446,7 +463,9 @@ async function handleApi(req, res, urlObj, body) {
// 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, 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),
channel: b.match?.channel || '',
accountId: b.match?.accountId || '',
@@ -461,7 +480,7 @@ async function handleApi(req, res, urlObj, body) {
// POST /api/agents/bot — create agent with its own bot token
if (method === 'POST' && pathname === '/api/agents/bot') {
const { botToken, agentId, name, model, workspace, purpose, personality } = body;
const { botToken, agentId, name, model, workspace, purpose, personality, skills, toolGroups } = body;
if (!botToken || !botToken.trim()) {
res.writeHead(400); res.end(JSON.stringify({ error: 'Bot Token is required' })); return;
}
@@ -507,6 +526,7 @@ async function handleApi(req, res, urlObj, body) {
const wsAlias = `~/.openclaw/workspaces/${workspace}`;
const agentEntry = { id: agentId, name: name || agentId, workspace: wsAlias };
if (model && model !== '__default__') agentEntry.model = { primary: model };
applySkillsTools(agentEntry, skills, toolGroups);
cfg.agents.list.push(agentEntry);
// Add binding
if (!cfg.bindings) cfg.bindings = [];
@@ -540,7 +560,7 @@ async function handleApi(req, res, urlObj, body) {
// POST /api/agents/discord — create top-level Discord agent (single bot, channel binding)
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)) {
res.writeHead(400); res.end(JSON.stringify({ error: 'Agent ID must contain only alphanumeric characters, underscores, or dashes' })); return;
}
@@ -566,6 +586,7 @@ async function handleApi(req, res, urlObj, body) {
const wsPath = path.join(OPENCLAW_DIR, 'workspaces', folder);
const agentEntry = { id: agentId, name: name || agentId, workspace: wsAlias };
if (model && model !== '__default__') agentEntry.model = { primary: model };
applySkillsTools(agentEntry, skills, toolGroups);
cfg.agents.list.push(agentEntry);
if (!cfg.bindings) cfg.bindings = [];
@@ -609,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)
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)) {
res.writeHead(400); res.end(JSON.stringify({ error: 'Agent ID must contain only alphanumeric characters, underscores, or dashes' })); return;
}
@@ -635,6 +656,7 @@ async function handleApi(req, res, urlObj, body) {
const wsPath = path.join(OPENCLAW_DIR, 'workspaces', folder);
const agentEntry = { id: agentId, name: displayName || agentId, workspace: wsAlias, parentAgentId: String(parentAgentId).trim() };
if (model && model !== '__default__') agentEntry.model = { primary: model };
applySkillsTools(agentEntry, skills, toolGroups);
cfg.agents.list.push(agentEntry);
if (!cfg.bindings) cfg.bindings = [];
@@ -679,7 +701,7 @@ async function handleApi(req, res, urlObj, body) {
// POST /api/agents — create sub-agent (shares parent bot)
if (method === 'POST' && pathname === '/api/agents') {
const { agentId, displayName, groupId, workspaceFolder, model, purpose, personality, initialMemory, parentAgentId, telegramUserId } = body;
const { agentId, displayName, groupId, workspaceFolder, model, purpose, personality, initialMemory, parentAgentId, telegramUserId, skills, toolGroups } = body;
if (!agentId || !/^[a-zA-Z0-9_-]+$/.test(agentId)) {
res.writeHead(400); res.end(JSON.stringify({ error: 'Agent ID must contain only alphanumeric characters, underscores, or dashes' })); return;
}
@@ -708,6 +730,7 @@ async function handleApi(req, res, urlObj, body) {
const wsAlias= `~/.openclaw/workspaces/${folder}`;
const agentEntry = { id: agentId, name: displayName || agentId, workspace: wsAlias, parentAgentId: String(parentAgentId).trim() };
if (model && model !== '__default__') agentEntry.model = { primary: model };
applySkillsTools(agentEntry, skills, toolGroups);
cfg.agents.list.push(agentEntry);
// Binding uses parent's accountId
const parentAccountId = parentBinding.match.accountId;
@@ -1726,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: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 */
.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; }
@@ -2564,6 +2600,9 @@ const I18N = {
'wiz.soul':'性格关键词','wiz.soulHint':'(逗号分隔,选填)','wiz.soulPh':'幽默、直接、有条理...',
'wiz.soulTip':'留空则使用默认成长型提示词(推荐)',
'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.soulYes':'含性格关键词','wiz.soulDefault':'默认成长型提示词(推荐)',
'wiz.autoBackup':'自动备份当前 openclaw.json ✓',
@@ -2705,6 +2744,9 @@ const I18N = {
'wiz.soul':'Personality Keywords','wiz.soulHint':'(comma-separated, optional)','wiz.soulPh':'humorous, direct, organized...',
'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.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.soulYes':'With personality keywords','wiz.soulDefault':'Default growth prompt (recommended)',
'wiz.autoBackup':'Auto-backup current openclaw.json ✓',
@@ -2984,6 +3026,75 @@ function showAddForm(type) {
applyLang();
}
// ── Skills / Tools picker helper ──────────────────────────
const AVAILABLE_SKILLS = [
{ id:'memory-continuity', label:'Memory Continuity', 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:'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() {
const modelOpts = buildModelOpts('__default__');
return '<div class="add-form">' +
@@ -3041,6 +3152,7 @@ function buildAddAgentForm() {
'<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 class="form-group">' + buildSkillsToolsPicker('fa') + '</div>' +
'<div style="display:flex;gap:8px;margin-top:14px">' +
'<button class="btn-primary" onclick="submitAddAgent()">' + t('agents.addAgentSubmit') + '</button>' +
'<button class="btn-ghost" onclick="clearAddForm()">' + t('btn.cancel') + '</button>' +
@@ -3136,6 +3248,7 @@ function buildAddSubForm() {
'<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>' +
'<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">' +
'<button class="btn-primary" onclick="submitAddSub()">' + t('agents.addSubSubmit') + '</button>' +
@@ -3169,18 +3282,20 @@ async function submitAddAgent() {
if (!name) { toast(t('agents.errName'), 'err'); return; }
if (!workspace) { toast('Workspace folder is required', 'err'); return; }
const { skills, toolGroups } = collectSkillsTools('fa');
try {
if (channel === 'telegram') {
const token = document.getElementById('fa-token').value.trim();
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);
} else {
const link = (document.getElementById('fa-discord-link')?.value||'').trim();
const parsed = parseDiscordLinkOrId(link);
if (!parsed) { toast('Discord channel link/ID is required', 'err'); return; }
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);
}
toast('Agent created successfully', 'ok');
@@ -3209,6 +3324,8 @@ async function submitAddSub() {
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) { toast(t('wiz.errIdFormat'), 'err'); return; }
if (!workspaceFolder) { toast('Workspace folder is required', 'err'); return; }
const { skills, toolGroups } = collectSkillsTools('fs');
try {
if (channel === 'telegram') {
const groupId = document.getElementById('fs-gid').value.trim();
@@ -3216,14 +3333,14 @@ async function submitAddSub() {
if (!groupId) { toast(t('wiz.errGroupId'), '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,
workspaceFolder, model: model === '__default__' ? '' : model, purpose, personality, initialMemory, telegramUserId });
workspaceFolder, model: model === '__default__' ? '' : model, purpose, personality, initialMemory, telegramUserId, skills, toolGroups });
} else {
const link = (document.getElementById('fs-discord-link')?.value||'').trim();
const parsed = parseDiscordLinkOrId(link);
if (!parsed) { toast('Discord thread link/ID is required', 'err'); return; }
await api('POST', '/api/agents/discord-sub', { parentAgentId, agentId, displayName: displayName || agentId,
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');
clearAddForm();
@@ -3240,16 +3357,24 @@ function renderAgents() {
// Build grouping: prefer explicit parentAgentId; fallback to telegram bot-root grouping.
const byId = {}; (S.agents||[]).forEach(a=>{ byId[a.id]=a; });
// infer parentAgentId for legacy telegram sub-agents if missing
// infer parentAgentId for sub-agents if missing
const accountToRootId = {};
(S.agents||[]).forEach(a=>{ if(a.hasOwnBot && a.accountId) accountToRootId[a.accountId]=a.id; });
(S.agents||[]).forEach(a=>{
if(!a.parentAgentId && !a.hasOwnBot && a.parentAccountId && accountToRootId[a.parentAccountId]){
if(a.parentAgentId || a.hasOwnBot || a.id === 'main') return; // already resolved or is a root
// Case 1: legacy telegram sub-agent with parentAccountId
if(a.parentAccountId && accountToRootId[a.parentAccountId]){
a._inferParentAgentId = accountToRootId[a.parentAccountId];
return;
}
// Case 2: orphan agent (no own bot, no parentAgentId, no binding-based inference)
// These are likely dynamically created sub-agents — default to 'main'
if(byId['main']){
a._inferParentAgentId = 'main';
}
});
const roots=[]; const childrenByRoot={}; const orphans=[];
const roots=[]; const childrenByRoot={};
(S.agents||[]).forEach(a=>{
const pid = a.parentAgentId || a._inferParentAgentId || '';
if(pid && byId[pid]){
@@ -3258,6 +3383,12 @@ function renderAgents() {
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){
const ch = (b.channel||'').toLowerCase();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openclaw-manager",
"version": "0.9.3",
"version": "1.0.0-rc1",
"description": "A local web UI for managing OpenClaw AI agents \u2014 no npm install required",
"main": "openclaw-manager.js",
"scripts": {