@@ -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.0 ' ;
const APP _VERSION = '0.9.4 ' ;
// --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' , '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 = [
{ 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 ) {
return new Promise ( ( resolve , reject ) => {
@@ -395,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 ;
@@ -424,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 || '' ,
@@ -439,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 ;
}
@@ -485,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 = [ ] ;
@@ -497,6 +539,7 @@ async function handleApi(req, res, urlObj, body) {
const agentName = name || agentId ;
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' , 'CURRENT_STATE.md' ) , generateCurrentStateMd ( ) , 'utf8' ) ;
// Create agents/<id>/ runtime directory (required by OpenClaw gateway)
const agentRuntimeDir = path . join ( OPENCLAW _DIR , 'agents' , agentId ) ;
await fsp . mkdir ( agentRuntimeDir , { recursive : true } ) ;
@@ -506,7 +549,7 @@ async function handleApi(req, res, urlObj, body) {
ok : true , agentId , workspacePath : wsPath , configBackup : bakPath ,
notes : [
'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 + '/' ,
'Configuration updated and backed up' ,
'Restart gateway to load new bot: openclaw gateway restart'
@@ -517,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 ;
}
@@ -543,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 = [ ] ;
@@ -565,6 +609,7 @@ async function handleApi(req, res, urlObj, body) {
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 , '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 ) ;
await fsp . mkdir ( agentRuntimeDir , { recursive : true } ) ;
@@ -575,7 +620,7 @@ async function handleApi(req, res, urlObj, body) {
notes : [
'Discord agent created (single Discord bot)' ,
'Channel binding added' ,
'Workspace + runtime directories created' ,
'Workspace + runtime directories created (including memory/CURRENT_STATE.md) ' ,
'Configuration updated and backed up' ,
'Restart gateway to apply: openclaw gateway restart'
]
@@ -585,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 ;
}
@@ -611,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 = [ ] ;
@@ -634,6 +680,7 @@ async function handleApi(req, res, urlObj, body) {
const name = displayName || agentId ;
await fsp . writeFile ( path . join ( wsPath , 'SOUL.md' ) , generateSoulMd ( name , purpose || '' , personality || '' ) , 'utf8' ) ;
await fsp . writeFile ( path . join ( wsPath , 'MEMORY.md' ) , generateMemoryMd ( name , initialMemory || '' ) , 'utf8' ) ;
await fsp . writeFile ( path . join ( wsPath , 'memory' , 'CURRENT_STATE.md' ) , generateCurrentStateMd ( ) , 'utf8' ) ;
const agentRuntimeDir = path . join ( OPENCLAW _DIR , 'agents' , agentId ) ;
await fsp . mkdir ( agentRuntimeDir , { recursive : true } ) ;
@@ -643,7 +690,7 @@ async function handleApi(req, res, urlObj, body) {
res . end ( JSON . stringify ( { ok : true , agentId , workspacePath : wsPath , configBackup : bakPath ,
notes : [
'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' ,
'Restart gateway to apply: openclaw gateway restart'
]
@@ -654,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 ;
}
@@ -683,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 ;
@@ -707,6 +755,7 @@ async function handleApi(req, res, urlObj, body) {
const name = displayName || agentId ;
await fsp . writeFile ( path . join ( wsPath , 'SOUL.md' ) , generateSoulMd ( name , purpose , personality ) , 'utf8' ) ;
await fsp . writeFile ( path . join ( wsPath , 'MEMORY.md' ) , generateMemoryMd ( name , initialMemory ) , 'utf8' ) ;
await fsp . writeFile ( path . join ( wsPath , 'memory' , 'CURRENT_STATE.md' ) , generateCurrentStateMd ( ) , 'utf8' ) ;
// Create agents/<id>/ runtime directory (required by OpenClaw gateway)
const agentRuntimeDir = path . join ( OPENCLAW _DIR , 'agents' , agentId ) ;
await fsp . mkdir ( agentRuntimeDir , { recursive : true } ) ;
@@ -715,7 +764,7 @@ async function handleApi(req, res, urlObj, body) {
res . end ( JSON . stringify ( { ok : true , agentId , workspacePath : wsPath , configBackup : bakPath ,
notes : [
'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' ,
'Restart gateway to apply: openclaw gateway restart'
] ,
@@ -1700,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 : 6 px ; padding : 5 px 10 px ; font - size : 12 px ; cursor : pointer ; }
. lang - toggle : hover { background : # 3 a3f5c ; color : var ( -- text ) ; }
/* skills/tools picker */
. skills - picker { background : var ( -- bg ) ; border : 1 px solid var ( -- border ) ; border - radius : 10 px ; padding : 12 px 14 px ; margin - top : 4 px ; }
. skills - picker summary { font - size : 13 px ; 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 : 1 fr 1 fr ; gap : 4 px 16 px ; margin - top : 8 px ; }
. skills - picker label . sp - item { display : flex ; align - items : center ; gap : 6 px ; font - size : 12.5 px ; color : var ( -- muted ) ; cursor : pointer ; padding : 3 px 0 ; }
. skills - picker label . sp - item : hover { color : var ( -- text ) ; }
. skills - picker label . sp - item input [ type = checkbox ] { accent - color : var ( -- accent ) ; width : 14 px ; height : 14 px ; }
. skills - picker . sp - actions { display : flex ; gap : 8 px ; margin - top : 8 px ; padding - top : 8 px ; border - top : 1 px solid var ( -- border ) ; }
. skills - picker . sp - actions button { font - size : 11 px ; padding : 3 px 10 px ; border - radius : 6 px ; border : 1 px 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 : 11 px ; font - weight : 700 ; color : var ( -- accent ) ; text - transform : uppercase ; letter - spacing : . 5 px ; margin : 8 px 0 4 px ; grid - column : 1 / - 1 ; }
/* centered version badge */
. top - version { position : absolute ; left : 50 % ; transform : translateX ( - 50 % ) ; font - size : 12 px ; font - weight : 800 ; letter - spacing : . 3 px ; color : var ( -- text ) ; background : rgba ( 108 , 99 , 255 , . 18 ) ; border : 1 px solid rgba ( 108 , 99 , 255 , . 45 ) ; padding : 4 px 12 px ; border - radius : 999 px ; box - shadow : 0 6 px 18 px rgba ( 0 , 0 , 0 , . 25 ) ; }
. ver - old { display : none ; }
@@ -2030,6 +2092,16 @@ const MAIN_HTML_BODY = String.raw`
< / d i v >
< / d i v >
< div class = "dash-gauges" id = "dashGauges" > < div class = "empty" > Loading ... < / d i v > < / d i v >
< div class = "card dash-notice" >
< h3 data - i18n = "dash.firstRunTitle" > 🚀 First - run checklist < / h 3 >
< ul class = "dash-note-list" >
< li data - i18n = "dash.firstRun1" > Confirm OCM is pointed at the correct OpenClaw directory . < / l i >
< li data - i18n = "dash.firstRun2" > Check this Dashboard first : make sure Gateway is running and the system looks healthy . < / l i >
< li data - i18n = "dash.firstRun3" > Open Agents / Routing and confirm the bindings you expect are actually there . < / l i >
< li data - i18n = "dash.firstRun4" > Use the built - in Terminal to run < code > openclaw doctor < / c o d e > o n c e s o y o u c a t c h e n v i r o n m e n t / a u t h i s s u e s e a r l y . < / l i >
< li data - i18n = "dash.firstRun5" > Before bigger edits , make a backup or check rollback so recovery is nearby if something goes wrong . < / l i >
< / u l >
< / d i v >
< div class = "dash-sections" >
< div class = "card dash-card" id = "dashSystem" >
< h3 > 🖥 ️ System Info < / h 3 >
@@ -2175,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)" >
< option value = "" data - i18n = "cli.presets" > ─ ─ 常用命令 ─ ─ < / o p t i o n >
< / s e l e c t >
< button class = "btn-secondary" style = "font-size:11px;padding:3px 10px" onclick = "copyCliCommand()" data - i18n = "cli.copy" > 复制命令 < / b u t t o n >
< button class = "btn-secondary" style = "font-size:11px;padding:3px 10px" onclick = "clearCliOutput()" data - i18n = "cli.clear" > 清空 < / b u t t o n >
< button class = "btn-secondary" style = "font-size:11px;padding:3px 10px" onclick = "toggleCliPanel()" data - i18n = "cli.collapse" > ▼ 收起 < / b u t t o n >
< / d i v >
@@ -2439,6 +2512,12 @@ const I18N = {
'models.onlyCliHint' : '模型下拉仅显示 openclaw models list 返回的模型。' ,
'models.modelListErr' : '读取 openclaw models list 失败:' ,
'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.notice1' : 'OCM 主要面向 Telegram:通过群组绑定 Agent,让每个 Agent 拥有独立 Workspace / SOUL.md / MEMORY.md。' ,
'dash.notice2' : '请确保你已有基本 OpenClaw 操作经验。OCM 主要负责可视化更新 openclaw.json,方便增删主 Agent 与 Sub-Agent,并支持多条 Agent 树。' ,
@@ -2521,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 ✓' ,
@@ -2534,7 +2616,7 @@ const I18N = {
'ch.peerId' : 'Peer ID' , 'ch.peerIdHint' : '(群组 ID 或用户 ID) ' , 'ch.peerIdTip' : '留空则匹配所有对应类型的 Peer' ,
'ch.submit' : '添加绑定' ,
'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.presets' : '── 常用命令 ──' , 'cli.builtins' : '内置命令' , 'cli.favs' : '我的收藏' ,
'cli.run' : '▶ 执行' , 'cli.stop' : '■ 停止' , 'cli.star' : '⭐' , 'cli.manage' : '管理' ,
@@ -2573,6 +2655,12 @@ const I18N = {
'models.onlyCliHint' : 'Model dropdowns only show IDs returned by openclaw models list.' ,
'models.modelListErr' : 'Failed to load openclaw models list: ' ,
'models.modelListEmpty' : 'No model IDs were parsed from openclaw models list. Run openclaw onboard first.' ,
'dash.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.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.' ,
@@ -2656,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 ✓' ,
@@ -2669,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.submit' : 'Add Binding' ,
'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.presets' : '── Presets ──' , 'cli.builtins' : 'Built-in' , 'cli.favs' : 'My Favorites' ,
'cli.run' : '▶ Run' , 'cli.stop' : '■ Stop' , 'cli.star' : '⭐' , 'cli.manage' : 'Manage' ,
@@ -2935,6 +3026,78 @@ function showAddForm(type) {
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 ( ) {
const modelOpts = buildModelOpts ( '__default__' ) ;
return '<div class="add-form">' +
@@ -2992,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>' +
'<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>' +
@@ -3087,6 +3251,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>' +
@@ -3120,18 +3285,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' ) ;
@@ -3160,6 +3327,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 ( ) ;
@@ -3167,14 +3336,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 ( ) ;
@@ -3191,16 +3360,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 ] ) {
@@ -3209,6 +3386,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 ( ) ;
@@ -3668,7 +3851,14 @@ async function deleteAuth(key){
}
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 ) ;
}
@@ -3908,8 +4098,8 @@ function runCli(){
es . addEventListener ( 'done' , e => {
try {
const d = JSON . parse ( e . data ) ;
if(d.code===0) cliAppend(' \\ n✅ 完成 (exit 0) \\ n', 'cli-done-ok');
else cliAppend(' \\ n❌ 退出码 '+d.code+' \\ n', 'cli-done-err');
if ( d . code === 0 ) cliAppend ( '\\n✅ Done (exit 0)\\n' , 'cli-done-ok' ) ;
else cliAppend ( '\\n❌ Exit code ' + d . code + '\\n' , 'cli-done-err' ) ;
} catch ( _ ) { }
es . close ( ) ; cliEvt = null ; setCliRunning ( false ) ;
} ) ;