hook-runner.js
4.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* 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.<hookName> (脚本路径)
*/
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<void>}
*/
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 };