- Restructure: pcexec/ and safe-restart/ → plugin/{tools,core,commands}
- New pcguard Go binary: validates AGENT_VERIFY, AGENT_ID, AGENT_WORKSPACE
- pcexec now injects AGENT_VERIFY env + appends openclaw bin to PATH
- plugin/index.ts: unified TypeScript entry point with resolveOpenclawPath()
- install.mjs: support --openclaw-profile-path, install pcguard, new paths
- README: updated structure docs + security limitations note
- Removed old root index.js and openclaw.plugin.json
183 lines
5.0 KiB
TypeScript
183 lines
5.0 KiB
TypeScript
import { StatusManager } from '../core/status-manager';
|
|
|
|
export interface SlashCommandOptions {
|
|
statusManager: StatusManager;
|
|
/** List of authorized user IDs */
|
|
authorizedUsers: string[];
|
|
/** Cooldown duration in seconds */
|
|
cooldownSeconds?: number;
|
|
/** Callback for replies */
|
|
onReply: (message: string) => Promise<void>;
|
|
}
|
|
|
|
interface CommandState {
|
|
passMgrEnabled: boolean;
|
|
safeRestartEnabled: boolean;
|
|
lastToggle: {
|
|
'pass-mgr': number;
|
|
'safe-restart': number;
|
|
};
|
|
}
|
|
|
|
export class SlashCommandHandler {
|
|
private statusManager: StatusManager;
|
|
private authorizedUsers: string[];
|
|
private cooldownMs: number;
|
|
private onReply: (message: string) => Promise<void>;
|
|
private state: CommandState;
|
|
|
|
constructor(options: SlashCommandOptions) {
|
|
this.statusManager = options.statusManager;
|
|
this.authorizedUsers = options.authorizedUsers;
|
|
this.cooldownMs = (options.cooldownSeconds || 10) * 1000;
|
|
this.onReply = options.onReply;
|
|
|
|
this.state = {
|
|
passMgrEnabled: true,
|
|
safeRestartEnabled: true,
|
|
lastToggle: {
|
|
'pass-mgr': 0,
|
|
'safe-restart': 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Handle a slash command
|
|
*/
|
|
async handle(command: string, userId: string): Promise<void> {
|
|
// Check authorization
|
|
if (!this.authorizedUsers.includes(userId)) {
|
|
await this.onReply('❌ 无权执行此命令');
|
|
return;
|
|
}
|
|
|
|
const parts = command.trim().split(/\s+/);
|
|
const subcommand = parts[1];
|
|
const feature = parts[2] as 'pass-mgr' | 'safe-restart';
|
|
|
|
switch (subcommand) {
|
|
case 'status':
|
|
await this.handleStatus();
|
|
break;
|
|
case 'enable':
|
|
await this.handleEnable(feature);
|
|
break;
|
|
case 'disable':
|
|
await this.handleDisable(feature);
|
|
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` - 禁用功能'
|
|
);
|
|
}
|
|
}
|
|
|
|
private async handleStatus(): Promise<void> {
|
|
const global = this.statusManager.getGlobalStatus();
|
|
const agents = this.statusManager.getAllAgents();
|
|
|
|
const lines = [
|
|
'**PaddedCell 状态**',
|
|
'',
|
|
`🔐 密码管理: ${this.state.passMgrEnabled ? '✅ 启用' : '❌ 禁用'}`,
|
|
`🔄 安全重启: ${this.state.safeRestartEnabled ? '✅ 启用' : '❌ 禁用'}`,
|
|
'',
|
|
'**Agent 状态:**',
|
|
];
|
|
|
|
for (const agent of agents) {
|
|
const emoji = this.getStateEmoji(agent.state);
|
|
lines.push(`${emoji} ${agent.agentId}: ${agent.state}`);
|
|
}
|
|
|
|
if (agents.length === 0) {
|
|
lines.push('(暂无 agent 注册)');
|
|
}
|
|
|
|
if (global.restartStatus !== 'idle') {
|
|
lines.push('');
|
|
lines.push(`⚠️ 重启状态: ${global.restartStatus}`);
|
|
if (global.restartScheduledBy) {
|
|
lines.push(` 由 ${global.restartScheduledBy} 发起`);
|
|
}
|
|
}
|
|
|
|
await this.onReply(lines.join('\n'));
|
|
}
|
|
|
|
private async handleEnable(feature: 'pass-mgr' | 'safe-restart'): Promise<void> {
|
|
if (!this.isValidFeature(feature)) {
|
|
await this.onReply('❌ 未知功能。可用选项: pass-mgr, safe-restart');
|
|
return;
|
|
}
|
|
|
|
if (this.isOnCooldown(feature)) {
|
|
await this.onReply('⏳ 该功能最近刚被修改过,请稍后再试');
|
|
return;
|
|
}
|
|
|
|
if (feature === 'pass-mgr') {
|
|
this.state.passMgrEnabled = true;
|
|
} else {
|
|
this.state.safeRestartEnabled = true;
|
|
}
|
|
|
|
this.state.lastToggle[feature] = Date.now();
|
|
await this.onReply(`✅ 已启用 ${feature}`);
|
|
}
|
|
|
|
private async handleDisable(feature: 'pass-mgr' | 'safe-restart'): Promise<void> {
|
|
if (!this.isValidFeature(feature)) {
|
|
await this.onReply('❌ 未知功能。可用选项: pass-mgr, safe-restart');
|
|
return;
|
|
}
|
|
|
|
if (this.isOnCooldown(feature)) {
|
|
await this.onReply('⏳ 该功能最近刚被修改过,请稍后再试');
|
|
return;
|
|
}
|
|
|
|
if (feature === 'pass-mgr') {
|
|
this.state.passMgrEnabled = false;
|
|
} else {
|
|
this.state.safeRestartEnabled = false;
|
|
}
|
|
|
|
this.state.lastToggle[feature] = Date.now();
|
|
await this.onReply(`✅ 已禁用 ${feature}`);
|
|
}
|
|
|
|
private isValidFeature(feature: string): feature is 'pass-mgr' | 'safe-restart' {
|
|
return feature === 'pass-mgr' || feature === 'safe-restart';
|
|
}
|
|
|
|
private isOnCooldown(feature: 'pass-mgr' | 'safe-restart'): boolean {
|
|
const lastToggle = this.state.lastToggle[feature];
|
|
return Date.now() - lastToggle < this.cooldownMs;
|
|
}
|
|
|
|
private getStateEmoji(state: string): string {
|
|
switch (state) {
|
|
case 'idle': return '💤';
|
|
case 'busy': return '⚡';
|
|
case 'focus': return '🎯';
|
|
case 'freeze': return '🧊';
|
|
case 'pre-freeze': return '⏳';
|
|
case 'pre-freeze-focus': return '📝';
|
|
default: return '❓';
|
|
}
|
|
}
|
|
|
|
isPassMgrEnabled(): boolean {
|
|
return this.state.passMgrEnabled;
|
|
}
|
|
|
|
isSafeRestartEnabled(): boolean {
|
|
return this.state.safeRestartEnabled;
|
|
}
|
|
}
|