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>
This commit is contained in:
dtzp555-max
2026-03-17 20:17:01 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.6
parent a371ba6f7c
commit 69258d8428
6 changed files with 158 additions and 14 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
+2 -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 ## 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. - **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.
@@ -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) ## 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**: screenshots grouped into a collapsible section so the homepage stays shorter. - **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.
+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.
+3 -1
View File
@@ -138,8 +138,9 @@ 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`.
--- ---
@@ -176,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)**
+3 -1
View File
@@ -123,8 +123,9 @@ 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`
--- ---
@@ -159,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`),
+118 -9
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.3'; 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: '使用全局默认模型' },
@@ -461,7 +472,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;
} }
@@ -507,6 +518,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 = [];
@@ -540,7 +552,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;
} }
@@ -566,6 +578,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 = [];
@@ -609,7 +622,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;
} }
@@ -635,6 +648,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 = [];
@@ -679,7 +693,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;
} }
@@ -708,6 +722,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;
@@ -1726,6 +1741,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; }
@@ -2564,6 +2592,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 ✓',
@@ -2984,6 +3015,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">' +
@@ -3041,6 +3144,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>' +
@@ -3136,6 +3240,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>' +
@@ -3169,18 +3274,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');
@@ -3209,6 +3316,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();
@@ -3216,14 +3325,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();