@ian2018cs/agenthub 0.1.75 → 0.1.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,316 @@
1
+ /**
2
+ * BackgroundTaskPool — 后台任务进程池
3
+ *
4
+ * 管理 child_process.spawn 子进程,提供 per-user 和全局并发限制、
5
+ * 超时处理、输出截断,以及 EventEmitter 'task-complete' 事件。
6
+ *
7
+ * PendingResultQueue — 待投递结果队列
8
+ *
9
+ * 保存已完成但尚未投递给 agent 的任务结果,
10
+ * 按 userUuid:sessionId 分组,支持逐条出队。
11
+ */
12
+
13
+ import { spawn } from 'child_process';
14
+ import { EventEmitter } from 'events';
15
+ import crypto from 'crypto';
16
+
17
+ // ─── 配置 ───
18
+
19
+ const PER_USER_LIMIT = parseInt(process.env.BG_TASK_PER_USER_LIMIT, 10) || 3;
20
+ const GLOBAL_LIMIT = parseInt(process.env.BG_TASK_GLOBAL_LIMIT, 10) || 20;
21
+ const DEFAULT_TIMEOUT = parseInt(process.env.BG_TASK_DEFAULT_TIMEOUT, 10) || 600_000; // 10 min
22
+ const MAX_TIMEOUT = parseInt(process.env.BG_TASK_MAX_TIMEOUT, 10) || 1_800_000; // 30 min
23
+ const MAX_OUTPUT_BYTES = parseInt(process.env.BG_TASK_MAX_OUTPUT, 10) || 102_400; // 100 KB
24
+
25
+ // 已完成任务保留时长(30 min),之后自动清理引用
26
+ const COMPLETED_TTL_MS = 30 * 60 * 1000;
27
+ // 清理扫描间隔(5 min)
28
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
29
+
30
+ // ─── BackgroundTaskPool ───
31
+
32
+ class BackgroundTaskPool extends EventEmitter {
33
+ constructor() {
34
+ super();
35
+ /** @type {Map<string, Task>} 所有任务(running + completed 未清理) */
36
+ this.tasks = new Map();
37
+ /** @type {Map<string, number>} 每用户当前 running 计数 */
38
+ this.userTaskCount = new Map();
39
+ /** 全局 running 计数 */
40
+ this.runningCount = 0;
41
+
42
+ // 定期清理已完成任务
43
+ this._cleanupTimer = setInterval(() => this._cleanupCompleted(), CLEANUP_INTERVAL_MS);
44
+ if (this._cleanupTimer.unref) this._cleanupTimer.unref();
45
+ }
46
+
47
+ /**
48
+ * 提交一个后台任务
49
+ * @returns {Task} 返回任务对象(status='running')
50
+ * @throws {Error} 超出并发限制
51
+ */
52
+ submit({ userUuid, sessionId, cwd, command, timeout, label }) {
53
+ // 并发检查
54
+ const userCount = this.userTaskCount.get(userUuid) || 0;
55
+ if (userCount >= PER_USER_LIMIT) {
56
+ throw new Error(`每用户最多同时运行 ${PER_USER_LIMIT} 个后台任务(当前 ${userCount} 个)`);
57
+ }
58
+ if (this.runningCount >= GLOBAL_LIMIT) {
59
+ throw new Error(`系统后台任务已满(最多 ${GLOBAL_LIMIT} 个)`);
60
+ }
61
+
62
+ // 规范化 timeout
63
+ const timeoutMs = Math.min(Math.max(timeout || DEFAULT_TIMEOUT, 1000), MAX_TIMEOUT);
64
+
65
+ const id = this._genId();
66
+ const task = {
67
+ id,
68
+ userUuid,
69
+ sessionId,
70
+ cwd,
71
+ command,
72
+ label: label || command.slice(0, 80),
73
+ timeout: timeoutMs,
74
+ status: 'running',
75
+ startTime: Date.now(),
76
+ childProcess: null,
77
+ stdout: '',
78
+ stderr: '',
79
+ exitCode: null,
80
+ signal: null,
81
+ truncated: false,
82
+ _stdoutTruncated: false,
83
+ _stderrTruncated: false,
84
+ };
85
+
86
+ // 启动子进程
87
+ const child = spawn('sh', ['-c', command], {
88
+ cwd,
89
+ stdio: ['ignore', 'pipe', 'pipe'],
90
+ env: { ...process.env },
91
+ });
92
+ task.childProcess = child;
93
+
94
+ // 收集 stdout
95
+ child.stdout.on('data', (chunk) => {
96
+ if (task._stdoutTruncated) return;
97
+ const str = chunk.toString();
98
+ if (task.stdout.length + str.length > MAX_OUTPUT_BYTES) {
99
+ task.stdout += str.slice(0, MAX_OUTPUT_BYTES - task.stdout.length);
100
+ task._stdoutTruncated = true;
101
+ task.truncated = true;
102
+ } else {
103
+ task.stdout += str;
104
+ }
105
+ });
106
+
107
+ // 收集 stderr
108
+ child.stderr.on('data', (chunk) => {
109
+ if (task._stderrTruncated) return;
110
+ const str = chunk.toString();
111
+ if (task.stderr.length + str.length > MAX_OUTPUT_BYTES) {
112
+ task.stderr += str.slice(0, MAX_OUTPUT_BYTES - task.stderr.length);
113
+ task._stderrTruncated = true;
114
+ task.truncated = true;
115
+ } else {
116
+ task.stderr += str;
117
+ }
118
+ });
119
+
120
+ // 进程结束
121
+ child.on('close', (code, sig) => {
122
+ if (task.status !== 'running') return; // 已被 timeout/cancel 处理
123
+ task.status = 'completed';
124
+ task.exitCode = code;
125
+ task.signal = sig;
126
+ this._onFinished(task);
127
+ });
128
+
129
+ child.on('error', (err) => {
130
+ if (task.status !== 'running') return;
131
+ task.status = 'failed';
132
+ task.stderr += `\n[spawn error] ${err.message}`;
133
+ this._onFinished(task);
134
+ });
135
+
136
+ // 超时定时器
137
+ task._timeoutTimer = setTimeout(() => {
138
+ if (task.status !== 'running') return;
139
+ task.status = 'timeout';
140
+ // 先 SIGTERM
141
+ try { child.kill('SIGTERM'); } catch (_) { /* ignore */ }
142
+ // 5s 后 SIGKILL
143
+ task._killTimer = setTimeout(() => {
144
+ try { child.kill('SIGKILL'); } catch (_) { /* ignore */ }
145
+ }, 5000);
146
+ this._onFinished(task);
147
+ }, timeoutMs);
148
+
149
+ // 注册
150
+ this.tasks.set(id, task);
151
+ this.userTaskCount.set(userUuid, userCount + 1);
152
+ this.runningCount++;
153
+
154
+ console.log(`[BgTask] Task ${id} started for user ${userUuid}, session ${sessionId}: ${task.label}`);
155
+ return task;
156
+ }
157
+
158
+ /**
159
+ * 取消一个任务
160
+ * @returns {boolean} 是否成功取消
161
+ */
162
+ cancel(taskId, userUuid) {
163
+ const task = this.tasks.get(taskId);
164
+ if (!task) return false;
165
+ if (task.userUuid !== userUuid) return false; // 不允许跨用户取消
166
+ if (task.status !== 'running') return false;
167
+
168
+ task.status = 'killed';
169
+ try { task.childProcess.kill('SIGTERM'); } catch (_) { /* ignore */ }
170
+ setTimeout(() => {
171
+ try { task.childProcess.kill('SIGKILL'); } catch (_) { /* ignore */ }
172
+ }, 3000);
173
+
174
+ this._onFinished(task);
175
+ return true;
176
+ }
177
+
178
+ /** 获取单个任务 */
179
+ getTask(taskId) {
180
+ return this.tasks.get(taskId) || null;
181
+ }
182
+
183
+ /** 获取用户所有任务 */
184
+ getUserTasks(userUuid) {
185
+ return [...this.tasks.values()].filter(t => t.userUuid === userUuid);
186
+ }
187
+
188
+ /** 获取指定 session 的任务 */
189
+ getSessionTasks(userUuid, sessionId) {
190
+ return [...this.tasks.values()].filter(t => t.userUuid === userUuid && t.sessionId === sessionId);
191
+ }
192
+
193
+ /** 统计信息 */
194
+ getStats() {
195
+ return {
196
+ running: this.runningCount,
197
+ total: this.tasks.size,
198
+ perUser: Object.fromEntries(this.userTaskCount),
199
+ };
200
+ }
201
+
202
+ // ─── 内部方法 ───
203
+
204
+ _onFinished(task) {
205
+ // 清理定时器
206
+ if (task._timeoutTimer) { clearTimeout(task._timeoutTimer); task._timeoutTimer = null; }
207
+ if (task._killTimer) { clearTimeout(task._killTimer); task._killTimer = null; }
208
+
209
+ // 更新计数
210
+ const prev = this.userTaskCount.get(task.userUuid) || 1;
211
+ if (prev <= 1) {
212
+ this.userTaskCount.delete(task.userUuid);
213
+ } else {
214
+ this.userTaskCount.set(task.userUuid, prev - 1);
215
+ }
216
+ this.runningCount = Math.max(0, this.runningCount - 1);
217
+
218
+ // 释放进程引用
219
+ task.childProcess = null;
220
+
221
+ // 记录完成时间
222
+ task.endTime = Date.now();
223
+
224
+ console.log(`[BgTask] Task ${task.id} finished: status=${task.status}, exitCode=${task.exitCode}, duration=${task.endTime - task.startTime}ms`);
225
+
226
+ // 发出事件
227
+ this.emit('task-complete', task);
228
+ }
229
+
230
+ _cleanupCompleted() {
231
+ const now = Date.now();
232
+ for (const [id, task] of this.tasks) {
233
+ if (task.status !== 'running' && task.endTime && now - task.endTime > COMPLETED_TTL_MS) {
234
+ this.tasks.delete(id);
235
+ }
236
+ }
237
+ }
238
+
239
+ _genId() {
240
+ return 'bg_' + crypto.randomBytes(6).toString('hex');
241
+ }
242
+ }
243
+
244
+ // ─── PendingResultQueue ───
245
+
246
+ /** @type {Map<string, object[]>} key = "userUuid:sessionId" */
247
+ const pendingResults = new Map();
248
+
249
+ function _key(userUuid, sessionId) {
250
+ return `${userUuid}:${sessionId}`;
251
+ }
252
+
253
+ /**
254
+ * 将已完成的任务结果加入待投递队列
255
+ */
256
+ export function enqueueResult(userUuid, sessionId, taskResult) {
257
+ const k = _key(userUuid, sessionId);
258
+ if (!pendingResults.has(k)) pendingResults.set(k, []);
259
+ pendingResults.get(k).push({
260
+ id: taskResult.id,
261
+ command: taskResult.command,
262
+ label: taskResult.label,
263
+ status: taskResult.status,
264
+ exitCode: taskResult.exitCode,
265
+ signal: taskResult.signal,
266
+ stdout: taskResult.stdout,
267
+ stderr: taskResult.stderr,
268
+ truncated: taskResult.truncated,
269
+ startTime: taskResult.startTime,
270
+ endTime: taskResult.endTime,
271
+ cwd: taskResult.cwd,
272
+ sessionId: taskResult.sessionId,
273
+ userUuid: taskResult.userUuid,
274
+ });
275
+ }
276
+
277
+ /**
278
+ * 从队列取出一条结果(FIFO)
279
+ */
280
+ export function dequeueResult(userUuid, sessionId) {
281
+ const k = _key(userUuid, sessionId);
282
+ const q = pendingResults.get(k);
283
+ if (!q || q.length === 0) return null;
284
+ const item = q.shift();
285
+ if (q.length === 0) pendingResults.delete(k);
286
+ return item;
287
+ }
288
+
289
+ /**
290
+ * 检查是否有待投递结果
291
+ */
292
+ export function hasResults(userUuid, sessionId) {
293
+ const k = _key(userUuid, sessionId);
294
+ const q = pendingResults.get(k);
295
+ return q && q.length > 0;
296
+ }
297
+
298
+ /**
299
+ * 获取用户所有 session 的待投递结果
300
+ * @returns {Array<{sessionId: string, results: object[]}>}
301
+ */
302
+ export function getAllPendingForUser(userUuid) {
303
+ const out = [];
304
+ for (const [k, results] of pendingResults) {
305
+ if (k.startsWith(userUuid + ':')) {
306
+ const sessionId = k.slice(userUuid.length + 1);
307
+ out.push({ sessionId, results: [...results] });
308
+ }
309
+ }
310
+ return out;
311
+ }
312
+
313
+ // ─── 单例导出 ───
314
+
315
+ export const backgroundTaskPool = new BackgroundTaskPool();
316
+ export default backgroundTaskPool;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * 内置工具:后台任务执行器 + 状态查询
3
+ *
4
+ * __bg_exec__: AI 调用 Bash(__bg_exec__ '<json>') 提交后台任务
5
+ * __bg_status__: AI 调用 Bash(__bg_status__) 或 Bash(__bg_status__ '<taskId>') 查询任务状态
6
+ *
7
+ * 飞书端不支持此工具(无法自动发起新 query 投递结果)。
8
+ */
9
+
10
+ import { backgroundTaskPool } from './background-task-pool.js';
11
+ import { evaluate as evaluateToolGuard } from '../tool-guard/index.js';
12
+
13
+ const ENABLED = process.env.BG_TASK_ENABLED !== 'false';
14
+
15
+ // ─── __bg_exec__:提交后台任务 ───
16
+
17
+ export default {
18
+ name: '__bg_exec__',
19
+
20
+ // 不需要 responseType — 结果通过 PendingResultQueue 投递,不走 resolveResponse 路径
21
+
22
+ systemPrompt: `
23
+
24
+ ## 后台任务执行器
25
+
26
+ 当你需要执行可能耗时超过 30 秒的命令时(如 git clone、npm install、大型构建、长时间脚本等),
27
+ 使用后台任务执行器代替 Bash 的 run_in_background + TaskOutput 轮询模式。
28
+
29
+ ### 提交后台任务
30
+ \`\`\`bash
31
+ __bg_exec__ '{"command":"要执行的命令","timeout":600000,"label":"任务描述"}'
32
+ \`\`\`
33
+ 参数说明:
34
+ - command(必需):要执行的 shell 命令
35
+ - timeout(可选):超时时间(毫秒),默认 600000(10分钟),最大 1800000(30分钟)
36
+ - label(可选):人类可读的任务描述
37
+
38
+ 任务会在后台执行,你会立即收到任务 ID 确认。任务完成后结果会自动推送给你,
39
+ 你不需要轮询或等待,可以继续处理其他工作。
40
+
41
+ ### 查询任务状态
42
+ \`\`\`bash
43
+ __bg_status__
44
+ \`\`\`
45
+ 不带参数:列出当前会话所有后台任务的状态。
46
+
47
+ \`\`\`bash
48
+ __bg_status__ bg_xxxxxxxxxxxx
49
+ \`\`\`
50
+ 带任务 ID:查询指定任务的详细状态(包含已产生的输出)。
51
+
52
+ 注意事项:
53
+ - 每个用户最多同时运行 3 个后台任务
54
+ - 不要对交互式命令使用此工具(如需要用户输入的命令)
55
+ - 不要对很快就能完成的命令使用此工具(<30 秒的用普通 Bash)
56
+ - 如果服务器重启,正在运行的后台任务会丢失
57
+ - 不要使用 TaskOutput 来查询后台任务状态,请使用 __bg_status__
58
+ `,
59
+
60
+ match(hookInput) {
61
+ if (!ENABLED) return false;
62
+ return hookInput.tool_name === 'Bash' &&
63
+ hookInput.tool_input?.command?.trimStart().startsWith('__bg_exec__');
64
+ },
65
+
66
+ async execute(hookInput, context) {
67
+ const { userUuid, mutableWriter, cwd } = context;
68
+
69
+ // 飞书模式检测
70
+ if (!mutableWriter?.current?.ws) {
71
+ return { decision: 'deny', reason: '后台任务执行功能仅支持网页端使用。' };
72
+ }
73
+
74
+ // 解析参数
75
+ const rawArgs = hookInput.tool_input.command.replace(/^\s*__bg_exec__\s*/, '');
76
+ let jsonStr = rawArgs.trim();
77
+ if ((jsonStr.startsWith("'") && jsonStr.endsWith("'")) ||
78
+ (jsonStr.startsWith('"') && jsonStr.endsWith('"'))) {
79
+ jsonStr = jsonStr.slice(1, -1);
80
+ }
81
+
82
+ let params;
83
+ try {
84
+ params = JSON.parse(jsonStr);
85
+ } catch (e) {
86
+ return { decision: 'deny', reason: `参数解析失败: ${e.message}。请使用 JSON 格式:__bg_exec__ '{"command":"..."}'` };
87
+ }
88
+
89
+ if (!params.command || typeof params.command !== 'string') {
90
+ return { decision: 'deny', reason: '缺少必需参数 command(要执行的命令)。' };
91
+ }
92
+
93
+ const actualCommand = params.command.trim();
94
+ if (!actualCommand) {
95
+ return { decision: 'deny', reason: 'command 不能为空。' };
96
+ }
97
+
98
+ // ToolGuard 安全检查(对实际命令而非 __bg_exec__ 包装)
99
+ try {
100
+ const guardResult = await evaluateToolGuard('Bash', { command: actualCommand }, {
101
+ userUuid,
102
+ cwd,
103
+ });
104
+ if (!guardResult.allowed) {
105
+ return { decision: 'deny', reason: `[系统安全策略] ${guardResult.reason}` };
106
+ }
107
+ } catch (err) {
108
+ console.error(`[BgTask] ToolGuard error:`, err.message);
109
+ // 守卫出错不阻塞,降级为允许
110
+ }
111
+
112
+ // 提交到进程池
113
+ let task;
114
+ try {
115
+ task = backgroundTaskPool.submit({
116
+ userUuid,
117
+ sessionId: mutableWriter.getSessionId(),
118
+ cwd,
119
+ command: actualCommand,
120
+ timeout: params.timeout,
121
+ label: params.label,
122
+ });
123
+ } catch (err) {
124
+ return { decision: 'deny', reason: `后台任务提交失败: ${err.message}` };
125
+ }
126
+
127
+ const timeoutSec = Math.round(task.timeout / 1000);
128
+ return {
129
+ decision: 'deny',
130
+ reason: `后台任务已启动。\n` +
131
+ `- 任务 ID: ${task.id}\n` +
132
+ `- 命令: ${actualCommand}\n` +
133
+ `- 超时: ${timeoutSec}s\n` +
134
+ `任务完成后结果会自动推送给你,请继续处理其他工作。`,
135
+ };
136
+ }
137
+ };
138
+
139
+ // ─── __bg_status__:查询后台任务状态 ───
140
+
141
+ function formatDuration(ms) {
142
+ if (ms < 1000) return `${ms}ms`;
143
+ const s = Math.round(ms / 1000);
144
+ if (s < 60) return `${s}s`;
145
+ return `${Math.floor(s / 60)}m${s % 60}s`;
146
+ }
147
+
148
+ function formatTaskSummary(task) {
149
+ const elapsed = (task.endTime || Date.now()) - task.startTime;
150
+ const lines = [
151
+ `[${task.id}] ${task.status.toUpperCase()}`,
152
+ ` 命令: ${task.command}`,
153
+ ` 耗时: ${formatDuration(elapsed)}`,
154
+ ];
155
+ if (task.status !== 'running') {
156
+ lines.push(` 退出码: ${task.exitCode ?? 'N/A'}`);
157
+ }
158
+ if (task.label && task.label !== task.command.slice(0, 80)) {
159
+ lines.splice(1, 0, ` 标签: ${task.label}`);
160
+ }
161
+ return lines.join('\n');
162
+ }
163
+
164
+ function formatTaskDetail(task) {
165
+ const elapsed = (task.endTime || Date.now()) - task.startTime;
166
+ const lines = [
167
+ `任务 ID: ${task.id}`,
168
+ `状态: ${task.status}`,
169
+ `命令: ${task.command}`,
170
+ `耗时: ${formatDuration(elapsed)}`,
171
+ `退出码: ${task.exitCode ?? 'N/A'}`,
172
+ ];
173
+ if (task.truncated) lines.push('(输出已截断至 100KB)');
174
+ if (task.stdout) lines.push('', '--- STDOUT ---', task.stdout);
175
+ if (task.stderr) lines.push('', '--- STDERR ---', task.stderr);
176
+ if (task.status === 'running' && !task.stdout && !task.stderr) {
177
+ lines.push('', '(暂无输出)');
178
+ }
179
+ return lines.join('\n');
180
+ }
181
+
182
+ export const bgStatusTool = {
183
+ name: '__bg_status__',
184
+
185
+ // systemPrompt 合并在 __bg_exec__ 的 systemPrompt 中,不需要单独的
186
+
187
+ systemPrompt: '',
188
+
189
+ match(hookInput) {
190
+ if (!ENABLED) return false;
191
+ return hookInput.tool_name === 'Bash' &&
192
+ hookInput.tool_input?.command?.trimStart().startsWith('__bg_status__');
193
+ },
194
+
195
+ async execute(hookInput, context) {
196
+ const { userUuid, mutableWriter } = context;
197
+ const sessionId = mutableWriter?.getSessionId();
198
+
199
+ // 解析可选的 taskId 参数
200
+ const rawArgs = hookInput.tool_input.command.replace(/^\s*__bg_status__\s*/, '').trim();
201
+ // 去掉可能的引号
202
+ let taskId = rawArgs.replace(/^['"]|['"]$/g, '').trim();
203
+
204
+ if (taskId) {
205
+ // 查询指定任务
206
+ const task = backgroundTaskPool.getTask(taskId);
207
+ if (!task) {
208
+ return { decision: 'deny', reason: `未找到任务 ${taskId}。任务可能已过期被清理。` };
209
+ }
210
+ if (task.userUuid !== userUuid) {
211
+ return { decision: 'deny', reason: `未找到任务 ${taskId}。` };
212
+ }
213
+ return { decision: 'deny', reason: formatTaskDetail(task) };
214
+ }
215
+
216
+ // 列出当前会话所有任务
217
+ const tasks = sessionId
218
+ ? backgroundTaskPool.getSessionTasks(userUuid, sessionId)
219
+ : backgroundTaskPool.getUserTasks(userUuid);
220
+
221
+ if (tasks.length === 0) {
222
+ return { decision: 'deny', reason: '当前没有后台任务。' };
223
+ }
224
+
225
+ const lines = [`后台任务列表(共 ${tasks.length} 个):`, ''];
226
+ for (const task of tasks) {
227
+ lines.push(formatTaskSummary(task), '');
228
+ }
229
+ return { decision: 'deny', reason: lines.join('\n') };
230
+ }
231
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Builtin Tool Registry — 内置工具注册中心
3
+ *
4
+ * claude-agent-sdk 不提供添加自定义内置工具的 API,因此通过
5
+ * "注入 system prompt + PreToolUse hook 拦截 Bash 命令"的方式模拟。
6
+ *
7
+ * 本模块采用 Registry + Strategy 模式:
8
+ * - 每个工具是一个自包含的策略对象(name, systemPrompt, match, execute)
9
+ * - Registry 统一管理注册、system prompt 合并、hook 分发、异步响应等待
10
+ *
11
+ * 扩展新工具只需在 tools/ 目录添加新模块并调用 registry.register()。
12
+ */
13
+
14
+ import crypto from 'crypto';
15
+
16
+ // ─── 通用工具 ───
17
+
18
+ /**
19
+ * 生成唯一请求 ID(用于 UI 审批/异步响应流)
20
+ */
21
+ export function createRequestId() {
22
+ if (typeof crypto.randomUUID === 'function') {
23
+ return crypto.randomUUID();
24
+ }
25
+ return crypto.randomBytes(16).toString('hex');
26
+ }
27
+
28
+ // ─── Registry ───
29
+
30
+ class BuiltinToolRegistry {
31
+ #tools = [];
32
+ #responseTypes = new Set();
33
+ #pendingRequests = new Map();
34
+
35
+ /**
36
+ * 注册一个内置工具
37
+ * @param {{ name: string, responseType?: string, systemPrompt: string, match: Function, execute: Function }} tool
38
+ */
39
+ register(tool) {
40
+ this.#tools.push(tool);
41
+ if (tool.responseType) {
42
+ this.#responseTypes.add(tool.responseType);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 合并所有工具的 systemPrompt 描述文本
48
+ */
49
+ getSystemPromptAppend() {
50
+ return this.#tools.map(t => t.systemPrompt).join('\n');
51
+ }
52
+
53
+ /**
54
+ * PreToolUse hook 入口:遍历工具匹配并执行
55
+ * @param {object} hookInput - SDK PreToolUse hook 输入
56
+ * @param {{ userUuid: string, cwd: string, mutableWriter: object }} ctx
57
+ * @returns {object|null} SDK hook 返回值,无匹配时返回 null
58
+ */
59
+ async handlePreToolUse(hookInput, ctx) {
60
+ for (const tool of this.#tools) {
61
+ if (tool.match(hookInput)) {
62
+ const toolContext = {
63
+ ...ctx,
64
+ createRequestId,
65
+ waitForResponse: (requestId, timeoutMs) => this.#waitForResponse(requestId, timeoutMs),
66
+ };
67
+
68
+ try {
69
+ const result = await tool.execute(hookInput, toolContext);
70
+ return {
71
+ hookSpecificOutput: {
72
+ hookEventName: 'PreToolUse',
73
+ permissionDecision: result.decision,
74
+ ...(result.reason && { permissionDecisionReason: result.reason }),
75
+ ...(result.updatedInput && { updatedInput: result.updatedInput }),
76
+ }
77
+ };
78
+ } catch (err) {
79
+ return {
80
+ hookSpecificOutput: {
81
+ hookEventName: 'PreToolUse',
82
+ permissionDecision: 'deny',
83
+ permissionDecisionReason: `内置工具 ${tool.name} 执行失败: ${err.message}`,
84
+ }
85
+ };
86
+ }
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * 检查 WS 消息类型是否属于内置工具的响应
94
+ */
95
+ canHandleResponse(type) {
96
+ return this.#responseTypes.has(type);
97
+ }
98
+
99
+ /**
100
+ * 解析异步等待中的 Promise(由 server/index.js WS 消息路由调用)
101
+ */
102
+ resolveResponse(requestId, data) {
103
+ const resolver = this.#pendingRequests.get(requestId);
104
+ if (resolver) resolver(data);
105
+ }
106
+
107
+ /**
108
+ * 带超时的异步响应等待
109
+ */
110
+ #waitForResponse(requestId, timeoutMs) {
111
+ return new Promise(resolve => {
112
+ let settled = false;
113
+ const finalize = (result) => {
114
+ if (settled) return;
115
+ settled = true;
116
+ this.#pendingRequests.delete(requestId);
117
+ clearTimeout(timeout);
118
+ resolve(result);
119
+ };
120
+ const timeout = setTimeout(() => finalize(null), timeoutMs);
121
+ this.#pendingRequests.set(requestId, finalize);
122
+ });
123
+ }
124
+ }
125
+
126
+ // ─── 单例创建 & 工具注册 ───
127
+
128
+ const registry = new BuiltinToolRegistry();
129
+
130
+ import shareProjectTemplate from './share-project-template.js';
131
+ registry.register(shareProjectTemplate);
132
+
133
+ import backgroundTask, { bgStatusTool } from './background-task.js';
134
+ registry.register(backgroundTask);
135
+ registry.register(bgStatusTool);
136
+
137
+ // 重新导出后台任务相关模块,供 server/index.js 使用
138
+ export {
139
+ backgroundTaskPool,
140
+ enqueueResult,
141
+ dequeueResult,
142
+ hasResults,
143
+ getAllPendingForUser,
144
+ } from './background-task-pool.js';
145
+
146
+ export default registry;