/** * HookRunner - 脚本化生命周期钩子执行器 (Orchestrator 侧) * * 职责: * 1. 解析 Agent 配置中的 hooks 字段 * 2. 注入上下文环境变量 (Context -> Env) * 3. 执行外部脚本 (Shell/Node/Python) * 4. 处理执行结果 (Block/Non-Block) * 5. Timeout 防挂起保护 * * 全局配置来源:config.hooks.defaultTimeout / config.hooks.timeout * Agent 级配置来源:agentConfig.hooks. (脚本路径) */ const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); /** 默认超时 30s */ const DEFAULT_TIMEOUT = 30000; class HookRunner { constructor(config, baseDir) { this.config = config; this.baseDir = baseDir || process.cwd(); // 全局 Timeout 配置 this.defaultTimeout = config?.hooks?.defaultTimeout || DEFAULT_TIMEOUT; this.timeouts = config?.hooks?.timeout || {}; // { "pre-task": 60000 } } /** * 执行指定的 Hook * @param {string} hookName - Hook 名称 (e.g. 'pre-task', 'post-task-success') * @param {object} agentConfig - Agent 配置对象 * @param {object} context - 上下文数据 (用于注入环境变量) * @returns {Promise} */ async run(hookName, agentConfig, context = {}) { const hookScript = agentConfig.hooks?.[hookName]; if (!hookScript) { return; // 未配置该 Hook,跳过 } const scriptPath = path.resolve(this.baseDir, hookScript); if (!fs.existsSync(scriptPath)) { console.warn(`[HookRunner] Warning: Hook script not found at ${scriptPath}`); return; } // 准备环境变量 const env = this._buildEnv(context); const timeout = this.timeouts[hookName] || this.defaultTimeout; console.log(`[HookRunner] 🟢 Executing ${hookName}: ${hookScript} (timeout: ${timeout}ms)`); const startTime = Date.now(); try { await this._spawnScript(scriptPath, env, timeout); const duration = Date.now() - startTime; console.log(`[HookRunner] ✅ ${hookName} completed in ${duration}ms`); } catch (err) { const duration = Date.now() - startTime; console.error(`[HookRunner] ❌ ${hookName} failed in ${duration}ms: ${err.message}`); // 对于阻塞型 Hook (pre-*, post-task-success),失败应抛出异常阻断流程 // 对于非阻塞型 (post-task-failure, post-session),通常只记录日志 if (this._isBlocking(hookName)) { throw err; } } } _isBlocking(hookName) { // 默认 pre-* 和 post-task-success 是阻塞的 return hookName.startsWith('pre-') || hookName === 'post-task-success'; } _buildEnv(context) { const env = { ...process.env }; // 转换 context 为大写下划线格式的环境变量 // e.g. agentName -> AGENT_NAME for (const [key, value] of Object.entries(context)) { if (value === undefined || value === null) continue; const envKey = key.replace(/([A-Z])/g, '_$1').toUpperCase(); env[envKey] = typeof value === 'object' ? JSON.stringify(value) : String(value); } return env; } _spawnScript(scriptPath, env, timeout) { return new Promise((resolve, reject) => { const child = spawn(scriptPath, [], { env, stdio: 'inherit', // 直接将脚本输出打印到主进程 shell: true // 支持运行 .sh, .py 等 }); let settled = false; // Timeout 保护 const timer = setTimeout(() => { if (settled) return; settled = true; console.error(`[HookRunner] ⏰ TIMEOUT after ${timeout}ms, killing...`); child.kill('SIGTERM'); setTimeout(() => { try { if (!child.killed) child.kill('SIGKILL'); } catch { } }, 500); reject(new Error(`Hook script timed out after ${timeout}ms`)); }, timeout); child.on('close', (code) => { clearTimeout(timer); if (settled) return; settled = true; if (code === 0) { resolve(); } else { reject(new Error(`Hook script exited with code ${code}`)); } }); child.on('error', (err) => { clearTimeout(timer); if (settled) return; settled = true; reject(err); }); }); } } module.exports = { HookRunner };