From 7346c80c882c3cb5d79adf92a080173fd2584d0d Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 24 Mar 2026 10:21:35 +0000 Subject: [PATCH] feat: add /ego-mgr slash command Add /ego-mgr slash command with subcommands: - get, set, list, delete, add-column, add-public-column, show - Uses pcexec to call ego-mgr binary with proper env vars Translate all Chinese text to English in responses. Note: pcexec tool name and function names remain unchanged. --- plugin/commands/ego-mgr-slash.ts | 214 ++++++++++++++++++++++++++++++ plugin/commands/slash-commands.ts | 36 ++--- plugin/index.ts | 24 ++++ plugin/openclaw.plugin.json | 2 +- plugin/tools/pcexec.ts | 4 +- 5 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 plugin/commands/ego-mgr-slash.ts diff --git a/plugin/commands/ego-mgr-slash.ts b/plugin/commands/ego-mgr-slash.ts new file mode 100644 index 0000000..16f4024 --- /dev/null +++ b/plugin/commands/ego-mgr-slash.ts @@ -0,0 +1,214 @@ +import { pcexec } from '../tools/pcexec'; + +export interface EgoMgrSlashCommandOptions { + /** OpenClaw base path */ + openclawPath: string; + /** Current agent ID */ + agentId: string; + /** Current workspace directory */ + workspaceDir: string; + /** Callback for replies */ + onReply: (message: string) => Promise; +} + +/** Sentinel value injected into every pcexec subprocess */ +const AGENT_VERIFY = 'IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE'; + +export class EgoMgrSlashCommand { + private openclawPath: string; + private agentId: string; + private workspaceDir: string; + private onReply: (message: string) => Promise; + private binDir: string; + + constructor(options: EgoMgrSlashCommandOptions) { + this.openclawPath = options.openclawPath; + this.agentId = options.agentId; + this.workspaceDir = options.workspaceDir; + this.onReply = options.onReply; + this.binDir = require('path').join(this.openclawPath, 'bin'); + } + + /** + * Handle /ego-mgr slash command + * @param command Full command string (e.g., "/ego-mgr get name") + */ + async handle(command: string): Promise { + const parts = command.trim().split(/\s+/); + // Remove the "/ego-mgr" prefix + const args = parts.slice(1); + const subcommand = args[0]; + + if (!subcommand) { + await this.showUsage(); + return; + } + + try { + switch (subcommand) { + case 'get': + await this.handleGet(args.slice(1)); + break; + case 'set': + await this.handleSet(args.slice(1)); + break; + case 'list': + await this.handleList(args.slice(1)); + break; + case 'delete': + await this.handleDelete(args.slice(1)); + break; + case 'add-column': + await this.handleAddColumn(args.slice(1)); + break; + case 'add-public-column': + await this.handleAddPublicColumn(args.slice(1)); + break; + case 'show': + await this.handleShow(); + break; + case 'help': + default: + await this.showUsage(); + } + } catch (error: any) { + await this.onReply(`Error: ${error.message || error}`); + } + } + + private async showUsage(): Promise { + const usage = [ + '**ego-mgr Commands**', + '', + '`/ego-mgr get ` - Get field value', + '`/ego-mgr set ` - Set field value', + '`/ego-mgr list` - List all field names', + '`/ego-mgr delete ` - Delete a field', + '`/ego-mgr add-column ` - Add an Agent Scope field', + '`/ego-mgr add-public-column ` - Add a Public Scope field', + '`/ego-mgr show` - Show all fields and values', + '', + 'Examples:', + '`/ego-mgr get name`', + '`/ego-mgr set timezone Asia/Shanghai`', + ].join('\n'); + await this.onReply(usage); + } + + private async handleGet(args: string[]): Promise { + if (args.length < 1) { + await this.onReply('Usage: `/ego-mgr get `'); + return; + } + const columnName = args[0]; + const result = await this.execEgoMgr(['get', columnName]); + await this.onReply(`**${columnName}**: ${result || '(empty)'}`); + } + + private async handleSet(args: string[]): Promise { + if (args.length < 2) { + await this.onReply('Usage: `/ego-mgr set `'); + return; + } + const columnName = args[0]; + const value = args.slice(1).join(' '); // Support values with spaces + await this.execEgoMgr(['set', columnName, value]); + await this.onReply(`Set **${columnName}** = \`${value}\``); + } + + private async handleList(args: string[]): Promise { + const result = await this.execEgoMgr(['list', 'columns']); + if (!result.trim()) { + await this.onReply('No fields defined'); + return; + } + const columns = result.split('\n').filter(Boolean); + const lines = ['**Fields**:', '']; + for (const col of columns) { + lines.push(`• ${col}`); + } + await this.onReply(lines.join('\n')); + } + + private async handleDelete(args: string[]): Promise { + if (args.length < 1) { + await this.onReply('Usage: `/ego-mgr delete `'); + return; + } + const columnName = args[0]; + await this.execEgoMgr(['delete', columnName]); + await this.onReply(`Deleted field **${columnName}**`); + } + + private async handleAddColumn(args: string[]): Promise { + if (args.length < 1) { + await this.onReply('Usage: `/ego-mgr add-column `'); + return; + } + const columnName = args[0]; + await this.execEgoMgr(['add', 'column', columnName]); + await this.onReply(`Added Agent Scope field **${columnName}**`); + } + + private async handleAddPublicColumn(args: string[]): Promise { + if (args.length < 1) { + await this.onReply('Usage: `/ego-mgr add-public-column `'); + return; + } + const columnName = args[0]; + await this.execEgoMgr(['add', 'public-column', columnName]); + await this.onReply(`Added Public Scope field **${columnName}**`); + } + + private async handleShow(): Promise { + const result = await this.execEgoMgr(['show']); + if (!result.trim()) { + await this.onReply('No field data'); + return; + } + const lines = ['**Field Data**:', '']; + lines.push('```'); + lines.push(result); + lines.push('```'); + await this.onReply(lines.join('\n')); + } + + /** + * Execute ego-mgr binary via pc-exec + */ + private async execEgoMgr(args: string[]): Promise { + const currentPath = process.env.PATH || ''; + const newPath = currentPath.includes(this.binDir) + ? currentPath + : `${currentPath}:${this.binDir}`; + + const command = `ego-mgr ${args.map(a => this.shellEscape(a)).join(' ')}`; + + const result = await pcexec(command, { + cwd: this.workspaceDir, + env: { + AGENT_ID: this.agentId, + AGENT_WORKSPACE: this.workspaceDir, + AGENT_VERIFY, + PATH: newPath, + }, + }); + + if (result.exitCode !== 0 && result.stderr) { + throw new Error(result.stderr); + } + + return result.stdout; + } + + /** + * Escape a string for shell usage + */ + private shellEscape(str: string): string { + // Simple escaping for common cases + if (/^[a-zA-Z0-9._-]+$/.test(str)) { + return str; + } + return `'${str.replace(/'/g, "'\"'\"'")}'`; + } +} diff --git a/plugin/commands/slash-commands.ts b/plugin/commands/slash-commands.ts index 4789dde..08f8481 100644 --- a/plugin/commands/slash-commands.ts +++ b/plugin/commands/slash-commands.ts @@ -48,7 +48,7 @@ export class SlashCommandHandler { async handle(command: string, userId: string): Promise { // Check authorization if (!this.authorizedUsers.includes(userId)) { - await this.onReply('❌ 无权执行此命令'); + await this.onReply('Unauthorized'); return; } @@ -68,10 +68,10 @@ export class SlashCommandHandler { break; default: await this.onReply( - '用法:\n' + - '`/padded-cell-ctrl status` - 查看状态\n' + - '`/padded-cell-ctrl enable pass-mgr|safe-restart` - 启用功能\n' + - '`/padded-cell-ctrl disable pass-mgr|safe-restart` - 禁用功能' + 'Usage:\n' + + '`/padded-cell-ctrl status` - Show status\n' + + '`/padded-cell-ctrl enable pass-mgr|safe-restart` - Enable feature\n' + + '`/padded-cell-ctrl disable pass-mgr|safe-restart` - Disable feature' ); } } @@ -81,12 +81,12 @@ export class SlashCommandHandler { const agents = this.statusManager.getAllAgents(); const lines = [ - '**PaddedCell 状态**', + '**PaddedCell Status**', '', - `🔐 密码管理: ${this.state.passMgrEnabled ? '✅ 启用' : '❌ 禁用'}`, - `🔄 安全重启: ${this.state.safeRestartEnabled ? '✅ 启用' : '❌ 禁用'}`, + `Secret Manager: ${this.state.passMgrEnabled ? 'Enabled' : 'Disabled'}`, + `Safe Restart: ${this.state.safeRestartEnabled ? 'Enabled' : 'Disabled'}`, '', - '**Agent 状态:**', + '**Agent Status:**', ]; for (const agent of agents) { @@ -95,14 +95,14 @@ export class SlashCommandHandler { } if (agents.length === 0) { - lines.push('(暂无 agent 注册)'); + lines.push('(No agents registered)'); } if (global.restartStatus !== 'idle') { lines.push(''); - lines.push(`⚠️ 重启状态: ${global.restartStatus}`); + lines.push(`Restart Status: ${global.restartStatus}`); if (global.restartScheduledBy) { - lines.push(` 由 ${global.restartScheduledBy} 发起`); + lines.push(` Initiated by ${global.restartScheduledBy}`); } } @@ -111,12 +111,12 @@ export class SlashCommandHandler { private async handleEnable(feature: 'pass-mgr' | 'safe-restart'): Promise { if (!this.isValidFeature(feature)) { - await this.onReply('❌ 未知功能。可用选项: pass-mgr, safe-restart'); + await this.onReply('Unknown feature. Available: pass-mgr, safe-restart'); return; } if (this.isOnCooldown(feature)) { - await this.onReply('⏳ 该功能最近刚被修改过,请稍后再试'); + await this.onReply('This feature was recently modified. Please try again later.'); return; } @@ -127,17 +127,17 @@ export class SlashCommandHandler { } this.state.lastToggle[feature] = Date.now(); - await this.onReply(`✅ 已启用 ${feature}`); + await this.onReply(`Enabled ${feature}`); } private async handleDisable(feature: 'pass-mgr' | 'safe-restart'): Promise { if (!this.isValidFeature(feature)) { - await this.onReply('❌ 未知功能。可用选项: pass-mgr, safe-restart'); + await this.onReply('Unknown feature. Available: pass-mgr, safe-restart'); return; } if (this.isOnCooldown(feature)) { - await this.onReply('⏳ 该功能最近刚被修改过,请稍后再试'); + await this.onReply('This feature was recently modified. Please try again later.'); return; } @@ -148,7 +148,7 @@ export class SlashCommandHandler { } this.state.lastToggle[feature] = Date.now(); - await this.onReply(`✅ 已禁用 ${feature}`); + await this.onReply(`Disabled ${feature}`); } private isValidFeature(feature: string): feature is 'pass-mgr' | 'safe-restart' { diff --git a/plugin/index.ts b/plugin/index.ts index 410e7c1..9c84e5b 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -10,6 +10,7 @@ import { startApiServer, } from './core/index'; import { SlashCommandHandler } from './commands/slash-commands'; +import { EgoMgrSlashCommand } from './commands/ego-mgr-slash'; /** Sentinel value injected into every pcexec subprocess */ const AGENT_VERIFY = 'IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE'; @@ -110,6 +111,28 @@ function register(api: any, config?: any) { }; }); + // Register /ego-mgr slash command + if (api.registerSlashCommand) { + api.registerSlashCommand({ + name: 'ego-mgr', + description: 'Manage agent identity/profile fields', + handler: async (ctx: any, command: string) => { + const egoMgrSlash = new EgoMgrSlashCommand({ + openclawPath, + agentId: ctx.agentId || '', + workspaceDir: ctx.workspaceDir || '', + onReply: async (message: string) => { + if (ctx.reply) { + await ctx.reply(message); + } + }, + }); + await egoMgrSlash.handle(command); + }, + }); + logger.info('Registered /ego-mgr slash command'); + } + logger.info('PaddedCell plugin initialized'); } @@ -125,4 +148,5 @@ module.exports.StatusManager = StatusManager; module.exports.createApiServer = createApiServer; module.exports.startApiServer = startApiServer; module.exports.SlashCommandHandler = SlashCommandHandler; +module.exports.EgoMgrSlashCommand = EgoMgrSlashCommand; module.exports.AGENT_VERIFY = AGENT_VERIFY; diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index d36ddb5..f6f98b6 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "padded-cell", "name": "PaddedCell", "version": "0.2.0", - "description": "Secure password management, safe execution, and coordinated agent restart", + "description": "Secure secret management, agent identity management, safe execution, and coordinated agent restart", "entry": "./index.js", "configSchema": { "type": "object", diff --git a/plugin/tools/pcexec.ts b/plugin/tools/pcexec.ts index cd9f12f..a056954 100644 --- a/plugin/tools/pcexec.ts +++ b/plugin/tools/pcexec.ts @@ -201,7 +201,7 @@ async function replaceSecretMgrGets( } /** - * Safe exec wrapper that handles pass_mgr get commands and sanitizes output. + * Safe exec wrapper that handles secret-mgr get commands and sanitizes output. */ export async function pcexec( command: string, @@ -297,7 +297,7 @@ export async function pcexec( /** * Synchronous version — password substitution is NOT supported here - * (use async pcexec for pass_mgr integration). + * (use async pcexec for secret-mgr integration). */ export function pcexecSync( command: string,