/** * Hook 管理器 * * 负责 hook 的注册、触发和管理 */ import { spawn } from 'child_process'; import { minimatch } from 'minimatch'; import type { Hooks, HookType, HookConfig, HookEvent, HookEventListener, ShellCommandConfig, FileHookConfig, ToolExecuteBeforeInput, ToolExecuteBeforeOutput, ToolExecuteAfterInput, ToolExecuteAfterOutput, SessionStartInput, SessionEndInput, MessageBeforeInput, MessageBeforeOutput, MessageAfterInput, FileChangeInput, FileChangeOutput, Plugin, PluginInput, } from './types.js'; /** * Hook 管理器 */ export class HookManager { /** 已注册的 hooks */ private hooks: Hooks[] = []; /** 配置型 hooks(从配置文件加载) */ private configHooks: HookConfig | null = null; /** 事件监听器 */ private eventListeners: HookEventListener[] = []; /** 当前工作目录 */ private workdir: string; /** 会话 ID */ private sessionId: string; constructor(workdir: string, sessionId?: string) { this.workdir = workdir; this.sessionId = sessionId || 'default'; } /** * 注册插件 */ async registerPlugin(plugin: Plugin): Promise { const input: PluginInput = { workdir: this.workdir, sessionId: this.sessionId, }; try { const hooks = await plugin(input); this.hooks.push(hooks); } catch (error) { console.error('Failed to register plugin:', error); } } /** * 注册 hooks 对象 */ registerHooks(hooks: Hooks): void { this.hooks.push(hooks); } /** * 设置配置型 hooks */ setConfigHooks(config: HookConfig): void { this.configHooks = config; } /** * 添加事件监听器 */ addEventListener(listener: HookEventListener): void { this.eventListeners.push(listener); } /** * 移除事件监听器 */ removeEventListener(listener: HookEventListener): void { const index = this.eventListeners.indexOf(listener); if (index !== -1) { this.eventListeners.splice(index, 1); } } /** * 发送事件 */ private emitEvent(type: HookType, data: unknown): void { const event: HookEvent = { type, timestamp: Date.now(), data, }; for (const listener of this.eventListeners) { try { listener(event); } catch (error) { console.error('Event listener error:', error); } } } /** * 触发工具执行前 hook */ async triggerToolExecuteBefore( input: ToolExecuteBeforeInput ): Promise { const output: ToolExecuteBeforeOutput = { args: { ...input.args }, }; for (const hook of this.hooks) { if (hook['tool.execute.before']) { try { await hook['tool.execute.before'](input, output); } catch (error) { console.error('Hook tool.execute.before error:', error); } } } this.emitEvent('tool.execute.before', { input, output }); return output; } /** * 触发工具执行后 hook */ async triggerToolExecuteAfter( input: ToolExecuteAfterInput, result: ToolExecuteAfterOutput['result'] ): Promise { const output: ToolExecuteAfterOutput = { result: { ...result }, }; for (const hook of this.hooks) { if (hook['tool.execute.after']) { try { await hook['tool.execute.after'](input, output); } catch (error) { console.error('Hook tool.execute.after error:', error); } } } this.emitEvent('tool.execute.after', { input, output }); return output; } /** * 触发会话开始 hook */ async triggerSessionStart(input: SessionStartInput): Promise { for (const hook of this.hooks) { if (hook['session.start']) { try { await hook['session.start'](input); } catch (error) { console.error('Hook session.start error:', error); } } } this.emitEvent('session.start', input); } /** * 触发会话结束 hook */ async triggerSessionEnd(input: SessionEndInput): Promise { for (const hook of this.hooks) { if (hook['session.end']) { try { await hook['session.end'](input); } catch (error) { console.error('Hook session.end error:', error); } } } // 执行配置型 session_completed hooks if (this.configHooks?.session_completed) { await this.executeShellCommands(this.configHooks.session_completed); } this.emitEvent('session.end', input); } /** * 触发消息前 hook */ async triggerMessageBefore( input: MessageBeforeInput ): Promise { const output: MessageBeforeOutput = { content: input.content, }; for (const hook of this.hooks) { if (hook['message.before']) { try { await hook['message.before'](input, output); } catch (error) { console.error('Hook message.before error:', error); } } } this.emitEvent('message.before', { input, output }); return output; } /** * 触发消息后 hook */ async triggerMessageAfter(input: MessageAfterInput): Promise { for (const hook of this.hooks) { if (hook['message.after']) { try { await hook['message.after'](input); } catch (error) { console.error('Hook message.after error:', error); } } } this.emitEvent('message.after', input); } /** * 触发文件编辑 hook */ async triggerFileEdited(input: FileChangeInput): Promise { const output: FileChangeOutput = {}; // 执行插件 hooks for (const hook of this.hooks) { if (hook['file.edited']) { try { await hook['file.edited'](input, output); } catch (error) { console.error('Hook file.edited error:', error); } } } // 执行配置型 hooks if (this.configHooks?.file_edited) { const results = await this.executeFileHooks( input.path, this.configHooks.file_edited ); output.commandResults = results; } this.emitEvent('file.edited', { input, output }); return output; } /** * 触发文件创建 hook */ async triggerFileCreated(input: FileChangeInput): Promise { const output: FileChangeOutput = {}; // 执行插件 hooks for (const hook of this.hooks) { if (hook['file.created']) { try { await hook['file.created'](input, output); } catch (error) { console.error('Hook file.created error:', error); } } } // 执行配置型 hooks if (this.configHooks?.file_created) { const results = await this.executeFileHooks( input.path, this.configHooks.file_created ); output.commandResults = results; } this.emitEvent('file.created', { input, output }); return output; } /** * 触发文件删除 hook */ async triggerFileDeleted(input: FileChangeInput): Promise { const output: FileChangeOutput = {}; // 执行插件 hooks for (const hook of this.hooks) { if (hook['file.deleted']) { try { await hook['file.deleted'](input, output); } catch (error) { console.error('Hook file.deleted error:', error); } } } // 执行配置型 hooks if (this.configHooks?.file_deleted) { const results = await this.executeFileHooks( input.path, this.configHooks.file_deleted ); output.commandResults = results; } this.emitEvent('file.deleted', { input, output }); return output; } /** * 执行文件 hooks * 根据文件路径匹配 glob 模式并执行对应命令 */ private async executeFileHooks( filePath: string, config: FileHookConfig ): Promise { const results: FileChangeOutput['commandResults'] = []; for (const [pattern, commands] of Object.entries(config)) { // 使用 minimatch 进行 glob 匹配 if (minimatch(filePath, pattern, { matchBase: true })) { const commandResults = await this.executeShellCommands(commands, { FILE_PATH: filePath, }); results.push(...commandResults); } } return results; } /** * 执行 shell 命令列表 */ private async executeShellCommands( commands: ShellCommandConfig[], extraEnv?: Record ): Promise> { const results: Array<{ command: string[]; success: boolean; output?: string; error?: string; }> = []; for (const cmdConfig of commands) { const result = await this.executeShellCommand(cmdConfig, extraEnv); results.push(result); } return results; } /** * 执行单个 shell 命令 */ private executeShellCommand( config: ShellCommandConfig, extraEnv?: Record ): Promise<{ command: string[]; success: boolean; output?: string; error?: string }> { return new Promise((resolve) => { const [cmd, ...args] = config.command; const timeout = config.timeout || 30000; const cwd = config.cwd || this.workdir; const env = { ...process.env, ...config.environment, ...extraEnv, }; let stdout = ''; let stderr = ''; const child = spawn(cmd, args, { cwd, env, shell: true, }); const timer = setTimeout(() => { child.kill('SIGTERM'); resolve({ command: config.command, success: false, error: `Command timed out after ${timeout}ms`, }); }, timeout); child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { clearTimeout(timer); resolve({ command: config.command, success: code === 0, output: stdout.trim() || undefined, error: stderr.trim() || undefined, }); }); child.on('error', (error) => { clearTimeout(timer); resolve({ command: config.command, success: false, error: error.message, }); }); }); } /** * 获取所有已注册的 hooks 数量 */ getHookCount(): number { return this.hooks.length; } /** * 清空所有 hooks */ clear(): void { this.hooks = []; this.configHooks = null; this.eventListeners = []; } } // 全局 Hook 管理器实例 let globalHookManager: HookManager | null = null; /** * 获取全局 Hook 管理器 */ export function getHookManager(): HookManager | null { return globalHookManager; } /** * 初始化全局 Hook 管理器 */ export function initHookManager(workdir: string, sessionId?: string): HookManager { globalHookManager = new HookManager(workdir, sessionId); return globalHookManager; } /** * 重置全局 Hook 管理器 */ export function resetHookManager(): void { if (globalHookManager) { globalHookManager.clear(); globalHookManager = null; } }