ai-helper-agent 1.3.4 → 1.4.0

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,369 @@
1
+ // skill/executor.js - Skill 执行引擎
2
+ // 解析 Skill 定义的步骤 DAG,按拓扑排序执行,支持并行
3
+ import { render } from './template.js';
4
+ import { readFileSync } from 'fs';
5
+ import { checkPath, checkCommand } from '../security.js';
6
+ import { join } from 'path';
7
+
8
+ // 执行上下文(当前活跃的执行任务)
9
+ const activeExecutions = new Map(); // execId → { skill, status, steps, results }
10
+ const MAX_ACTIVE_EXECUTIONS = 100;
11
+ const EXECUTION_TIMEOUT = 300000; // 5 分钟超时
12
+ const EXECUTION_CLEANUP_AGE = 600000; // 10 分钟后清理
13
+
14
+ let execIdCounter = 0;
15
+
16
+ /**
17
+ * 清理超时/过期的执行记录
18
+ */
19
+ function cleanupActiveExecutions() {
20
+ const now = Date.now();
21
+ // 删除超时的记录
22
+ for (const [execId, exec] of activeExecutions) {
23
+ if (exec.status === 'completed' || exec.status === 'error') {
24
+ const age = now - (exec.endTime || exec.startTime);
25
+ if (age > EXECUTION_CLEANUP_AGE) {
26
+ activeExecutions.delete(execId);
27
+ }
28
+ }
29
+ }
30
+ // 超过上限时,删除最旧的已完成记录
31
+ if (activeExecutions.size > MAX_ACTIVE_EXECUTIONS) {
32
+ const sorted = [...activeExecutions.entries()]
33
+ .filter(([_, e]) => e.status === 'completed' || e.status === 'error')
34
+ .sort((a, b) => a[1].startTime - b[1].startTime);
35
+ const toDelete = sorted.slice(0, sorted.length - MAX_ACTIVE_EXECUTIONS / 2);
36
+ for (const [execId] of toDelete) {
37
+ activeExecutions.delete(execId);
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 内部工具调用函数
44
+ * 每个 Skill 步骤都是一个 Agent 工具调用
45
+ */
46
+ async function executeToolCall(toolName, args) {
47
+ // 映射 Skill 步骤中的 tool 到实际 Agent API
48
+ try {
49
+ switch (toolName) {
50
+ case 'agent_read_file': {
51
+ const check = checkPath(args.path);
52
+ if (!check.allowed) return { success: false, error: check.reason };
53
+ const content = readFileSync(check.resolved, 'utf-8');
54
+ return { success: true, content, size: content.length, path: check.resolved };
55
+ }
56
+
57
+ case 'agent_write_file': {
58
+ const check = checkPath(args.path);
59
+ if (!check.allowed) return { success: false, error: check.reason };
60
+ const { writeFileSync } = await import('fs');
61
+ writeFileSync(check.resolved, String(args.content || ''), 'utf-8');
62
+ return { success: true, message: `文件已写入: ${check.resolved}` };
63
+ }
64
+
65
+ case 'agent_list_dir': {
66
+ const check = checkPath(args.path || '.');
67
+ if (!check.allowed) return { success: false, error: check.reason };
68
+ const { readdirSync, statSync } = await import('fs');
69
+ const entries = readdirSync(check.resolved).map(name => {
70
+ const s = statSync(join(check.resolved, name));
71
+ return { name, type: s.isDirectory() ? 'directory' : 'file', size: s.size };
72
+ });
73
+ return { success: true, entries, path: check.resolved };
74
+ }
75
+
76
+ case 'agent_exec_command': {
77
+ const cmdCheck = checkCommand(args.command, false);
78
+ if (cmdCheck.level === 'deny') {
79
+ return { success: false, error: cmdCheck.reason };
80
+ }
81
+ // Workflow Skill 自动执行,没有交互确认环节,灰名单命令直接拒绝
82
+ if (cmdCheck.level === 'confirm') {
83
+ return { success: false, error: `需要用户确认的命令: ${cmdCheck.reason}` };
84
+ }
85
+ const { execSync } = await import('child_process');
86
+ const result = execSync(args.command, {
87
+ cwd: args.cwd || process.cwd(),
88
+ encoding: 'utf-8',
89
+ timeout: 30000,
90
+ maxBuffer: 10 * 1024 * 1024
91
+ });
92
+ return { success: true, stdout: result, exitCode: 0 };
93
+ }
94
+
95
+ case 'agent_search_files': {
96
+ const { searchFiles } = await import('../search.js');
97
+ return await searchFiles(
98
+ args.path || '.',
99
+ args.pattern || '*',
100
+ args.recursive !== false,
101
+ Math.min(args.maxResults || 200, 5000)
102
+ );
103
+ }
104
+
105
+ case 'agent_search_content': {
106
+ const { searchContent } = await import('../search.js');
107
+ return await searchContent(
108
+ args.path || '.',
109
+ args.pattern,
110
+ args.filePattern || null,
111
+ args.caseSensitive || false,
112
+ Math.min(args.maxResults || 100, 5000),
113
+ args.contextLines || 2
114
+ );
115
+ }
116
+
117
+ default:
118
+ return { success: false, error: `未知工具: ${toolName}` };
119
+ }
120
+ } catch (err) {
121
+ return { success: false, error: err.message };
122
+ }
123
+ }
124
+
125
+ /**
126
+ * 构建步骤依赖 DAG
127
+ * @returns {{ graph: Map<string, string[]>, inDegree: Map<string, number> }}
128
+ */
129
+ function buildStepDag(steps) {
130
+ const graph = new Map();
131
+ const inDegree = new Map();
132
+
133
+ for (const step of steps) {
134
+ const stepId = step.id;
135
+ if (!graph.has(stepId)) {
136
+ graph.set(stepId, []);
137
+ inDegree.set(stepId, 0);
138
+ }
139
+
140
+ if (step.dependsOn && Array.isArray(step.dependsOn)) {
141
+ for (const depId of step.dependsOn) {
142
+ if (!graph.has(depId)) {
143
+ graph.set(depId, []);
144
+ inDegree.set(depId, 0);
145
+ }
146
+ graph.get(depId).push(stepId);
147
+ inDegree.set(stepId, (inDegree.get(stepId) || 0) + 1);
148
+ }
149
+ }
150
+ }
151
+
152
+ return { graph, inDegree };
153
+ }
154
+
155
+ /**
156
+ * 按拓扑排序执行步骤(支持并行)
157
+ * @param {Object} skill - Skill 定义
158
+ * @param {Object} params - 用户传入的参数
159
+ * @param {string} execId - 执行 ID
160
+ * @param {Function} onStepUpdate - 步骤状态回调
161
+ */
162
+ async function executeSkillSteps(skill, params, execId, onStepUpdate) {
163
+ const { graph, inDegree } = buildStepDag(skill.steps);
164
+ const stepResults = {};
165
+ const completed = new Set();
166
+
167
+ // 合并默认参数和用户传入参数
168
+ const variables = { ...params };
169
+ if (skill.parameters) {
170
+ for (const [key, def] of Object.entries(skill.parameters)) {
171
+ if (variables[key] === undefined && def.default !== undefined) {
172
+ variables[key] = def.default;
173
+ }
174
+ }
175
+ }
176
+
177
+ // 准备就绪队列
178
+ let ready = [];
179
+ for (const step of skill.steps) {
180
+ if (inDegree.get(step.id) === 0) {
181
+ ready.push(step);
182
+ }
183
+ }
184
+
185
+ // 拓扑排序执行
186
+ while (ready.length > 0) {
187
+ // 并行执行当前批次的所有就绪步骤
188
+ const batch = ready;
189
+ ready = [];
190
+
191
+ const batchPromises = batch.map(async (step) => {
192
+ try {
193
+ // 检查条件
194
+ if (step.when !== undefined) {
195
+ const whenResult = render(step.when, variables);
196
+ if (!whenResult || whenResult === 'false' || whenResult === false) {
197
+ onStepUpdate(step.id, 'skipped', '条件不满足');
198
+ completed.add(step.id);
199
+ return;
200
+ }
201
+ }
202
+
203
+ onStepUpdate(step.id, 'running', '执行中...');
204
+
205
+ // 渲染参数中的模板变量(包含前面步骤的结果)
206
+ const stepArgs = step.args || step.params || {};
207
+ const renderedParams = { ...stepArgs };
208
+ for (const [key, value] of Object.entries(renderedParams)) {
209
+ if (typeof value === 'string') {
210
+ renderedParams[key] = render(value, variables);
211
+ }
212
+ }
213
+
214
+ // 执行工具调用
215
+ const result = await executeToolCall(step.tool, renderedParams);
216
+
217
+ // 存储结果到变量上下文
218
+ stepResults[step.id] = result;
219
+ variables[`step.${step.id}`] = result;
220
+ variables[`step.${step.id}.success`] = result.success;
221
+ variables[`step.${step.id}.content`] = result.content || '';
222
+
223
+ if (result.success) {
224
+ onStepUpdate(step.id, 'success', result.content || '执行成功');
225
+ } else {
226
+ onStepUpdate(step.id, 'error', result.error || '执行失败');
227
+ }
228
+
229
+ completed.add(step.id);
230
+ } catch (err) {
231
+ stepResults[step.id] = { success: false, error: err.message };
232
+ onStepUpdate(step.id, 'error', err.message);
233
+ completed.add(step.id);
234
+ }
235
+ });
236
+
237
+ await Promise.all(batchPromises);
238
+
239
+ // 查找新的就绪步骤
240
+ for (const step of skill.steps) {
241
+ if (completed.has(step.id)) continue;
242
+
243
+ const deps = step.dependsOn || [];
244
+ const allDepsDone = deps.every(depId => completed.has(depId));
245
+ if (allDepsDone) {
246
+ ready.push(step);
247
+ }
248
+ }
249
+
250
+ // 防止死循环:如果没有任何步骤变为就绪但还有未完成的步骤
251
+ if (ready.length === 0 && completed.size < skill.steps.length) {
252
+ console.warn('[Skill Executor] 可能存在循环依赖或缺失的依赖步骤');
253
+ break;
254
+ }
255
+ }
256
+
257
+ return stepResults;
258
+ }
259
+
260
+ /**
261
+ * 执行一个 Skill
262
+ * @param {Object} skill - Skill 定义(从 registry 获取)
263
+ * @param {Object} params - 用户传入的参数
264
+ * @param {Function} [onStepUpdate] - 步骤状态回调 (stepId, status, message)
265
+ * @returns {Promise<{success: boolean, execId: string, results: Object}>}
266
+ */
267
+ export async function executeSkill(skill, params = {}, onStepUpdate) {
268
+ const execId = `skill_${++execIdCounter}`;
269
+
270
+ // 参数校验
271
+ if (skill.parameters) {
272
+ // 兼容 JSON Schema 格式: { type: "object", properties: {...}, required: [...] }
273
+ const props = skill.parameters.properties || skill.parameters;
274
+ const requiredList = Array.isArray(skill.parameters.required) ? skill.parameters.required : [];
275
+ for (const [key, def] of Object.entries(props)) {
276
+ if (!def || typeof def !== 'object') continue;
277
+ if (requiredList.includes(key) && (params[key] === undefined || params[key] === null || params[key] === '')) {
278
+ return { success: false, execId, error: `缺少必需参数: ${key}` };
279
+ }
280
+ if (params[key] !== undefined && def.type) {
281
+ // 基本类型检查
282
+ const actualType = typeof params[key];
283
+ if (def.type === 'number' && actualType !== 'number') {
284
+ params[key] = Number(params[key]);
285
+ } else if (def.type === 'boolean' && actualType !== 'boolean') {
286
+ params[key] = params[key] === 'true' || params[key] === true;
287
+ } else if (def.type === 'string' && actualType !== 'string') {
288
+ params[key] = String(params[key]);
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ const execution = {
295
+ skill: skill.name,
296
+ execId,
297
+ status: 'running',
298
+ steps: {},
299
+ startTime: Date.now()
300
+ };
301
+
302
+ activeExecutions.set(execId, execution);
303
+ cleanupActiveExecutions(); // 执行前先清理过期记录
304
+
305
+ const notify = (stepId, status, message) => {
306
+ execution.steps[stepId] = { status, message };
307
+ if (onStepUpdate) onStepUpdate(stepId, status, message);
308
+ };
309
+
310
+ try {
311
+ // 带超时的执行
312
+ const results = await Promise.race([
313
+ executeSkillSteps(skill, params, execId, notify),
314
+ new Promise((_, reject) =>
315
+ setTimeout(() => reject(new Error('Skill 执行超时(5 分钟)')), EXECUTION_TIMEOUT)
316
+ )
317
+ ]);
318
+
319
+ // 检查是否有失败的步骤
320
+ const failedSteps = Object.entries(results).filter(([_, r]) => !r.success);
321
+ if (failedSteps.length > 0) {
322
+ execution.status = 'partial';
323
+ execution.results = results;
324
+ activeExecutions.set(execId, execution);
325
+ return {
326
+ success: true,
327
+ execId,
328
+ partial: true,
329
+ results,
330
+ message: `Skill "${skill.name}" 执行完成(${failedSteps.length} 个步骤失败)`
331
+ };
332
+ }
333
+
334
+ execution.status = 'completed';
335
+ execution.results = results;
336
+ execution.endTime = Date.now();
337
+ activeExecutions.set(execId, execution);
338
+
339
+ return {
340
+ success: true,
341
+ execId,
342
+ results,
343
+ message: `Skill "${skill.name}" 执行完成(${Object.keys(results).length} 个步骤)`
344
+ };
345
+ } catch (err) {
346
+ execution.status = 'error';
347
+ execution.error = err.message;
348
+ activeExecutions.set(execId, execution);
349
+ return { success: false, execId, error: `执行异常: ${err.message}` };
350
+ }
351
+ }
352
+
353
+ /**
354
+ * 查询 Skill 执行状态
355
+ */
356
+ export function getSkillExecutionStatus(execId) {
357
+ const execution = activeExecutions.get(execId);
358
+ if (!execution) {
359
+ return { success: false, error: `执行 ID "${execId}" 不存在` };
360
+ }
361
+ return {
362
+ success: true,
363
+ execId: execution.execId,
364
+ skill: execution.skill,
365
+ status: execution.status,
366
+ steps: execution.steps,
367
+ elapsed: Date.now() - execution.startTime
368
+ };
369
+ }