foliko 1.0.75 → 1.0.76
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.
- package/.claude/settings.local.json +159 -157
- package/cli/bin/foliko.js +12 -12
- package/cli/src/commands/chat.js +143 -143
- package/cli/src/commands/list.js +93 -93
- package/cli/src/index.js +75 -75
- package/cli/src/ui/chat-ui.js +201 -201
- package/cli/src/utils/ansi.js +40 -40
- package/cli/src/utils/markdown.js +292 -292
- package/examples/ambient-example.js +194 -194
- package/examples/basic.js +115 -115
- package/examples/bootstrap.js +121 -121
- package/examples/mcp-example.js +56 -56
- package/examples/skill-example.js +49 -49
- package/examples/test-chat.js +137 -137
- package/examples/test-mcp.js +85 -85
- package/examples/test-reload.js +59 -59
- package/examples/test-telegram.js +50 -50
- package/examples/test-tg-bot.js +45 -45
- package/examples/test-tg-simple.js +47 -47
- package/examples/test-tg.js +62 -62
- package/examples/test-think.js +43 -43
- package/examples/test-web-plugin.js +103 -103
- package/examples/test-weixin-feishu.js +103 -103
- package/examples/workflow.js +158 -158
- package/package.json +1 -1
- package/plugins/ai-plugin.js +102 -102
- package/plugins/ambient-agent/EventWatcher.js +113 -113
- package/plugins/ambient-agent/ExplorerLoop.js +640 -640
- package/plugins/ambient-agent/GoalManager.js +197 -197
- package/plugins/ambient-agent/Reflector.js +95 -95
- package/plugins/ambient-agent/StateStore.js +90 -90
- package/plugins/ambient-agent/constants.js +101 -101
- package/plugins/ambient-agent/index.js +579 -579
- package/plugins/audit-plugin.js +187 -187
- package/plugins/default-plugins.js +662 -662
- package/plugins/email/constants.js +64 -64
- package/plugins/email/handlers.js +461 -461
- package/plugins/email/index.js +278 -278
- package/plugins/email/monitor.js +269 -269
- package/plugins/email/parser.js +138 -138
- package/plugins/email/reply.js +151 -151
- package/plugins/email/utils.js +124 -124
- package/plugins/feishu-plugin.js +481 -481
- package/plugins/file-system-plugin.js +826 -826
- package/plugins/install-plugin.js +199 -199
- package/plugins/python-executor-plugin.js +367 -367
- package/plugins/python-plugin-loader.js +481 -481
- package/plugins/rules-plugin.js +294 -294
- package/plugins/scheduler-plugin.js +691 -691
- package/plugins/session-plugin.js +369 -369
- package/plugins/shell-executor-plugin.js +197 -197
- package/plugins/storage-plugin.js +240 -240
- package/plugins/subagent-plugin.js +845 -845
- package/plugins/telegram-plugin.js +482 -482
- package/plugins/think-plugin.js +345 -345
- package/plugins/tools-plugin.js +196 -196
- package/plugins/web-plugin.js +606 -606
- package/plugins/weixin-plugin.js +545 -545
- package/src/capabilities/index.js +11 -11
- package/src/capabilities/skill-manager.js +609 -609
- package/src/capabilities/workflow-engine.js +1109 -1109
- package/src/core/agent-chat.js +882 -882
- package/src/core/agent.js +892 -892
- package/src/core/framework.js +465 -465
- package/src/core/index.js +19 -19
- package/src/core/plugin-base.js +219 -219
- package/src/core/plugin-manager.js +863 -863
- package/src/core/provider.js +114 -114
- package/src/core/sub-agent-config.js +264 -264
- package/src/core/system-prompt-builder.js +120 -120
- package/src/core/tool-registry.js +517 -517
- package/src/core/tool-router.js +297 -297
- package/src/executors/executor-base.js +58 -58
- package/src/executors/mcp-executor.js +741 -741
- package/src/index.js +25 -25
- package/src/utils/circuit-breaker.js +301 -301
- package/src/utils/error-boundary.js +363 -363
- package/src/utils/error.js +374 -374
- package/src/utils/event-emitter.js +97 -97
- package/src/utils/id.js +133 -133
- package/src/utils/index.js +217 -217
- package/src/utils/logger.js +181 -181
- package/src/utils/plugin-helpers.js +90 -90
- package/src/utils/retry.js +122 -122
- package/src/utils/sandbox.js +292 -292
- package/test/tool-registry-validation.test.js +218 -218
- package/website/script.js +136 -136
- package/foliko-1.0.75.tgz +0 -0
|
@@ -1,609 +1,609 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SkillManager 技能管理器
|
|
3
|
-
* 加载和管理 Skill
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const { Plugin } = require('../core/plugin-base');
|
|
9
|
-
const { logger } = require('../utils/logger');
|
|
10
|
-
const log = logger.child('SkillManager');
|
|
11
|
-
const { z } = require('zod');
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 验证 skill 名称
|
|
15
|
-
* 1-64字符,字母数字、下划线和连字符,不能以连字符或下划线开头或结尾
|
|
16
|
-
*/
|
|
17
|
-
function isValidSkillName(name) {
|
|
18
|
-
if (!name || name.length < 1 || name.length > 64) return false;
|
|
19
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(name)) return false;
|
|
20
|
-
if (name.startsWith('-') || name.startsWith('_') || name.endsWith('-') || name.endsWith('_'))
|
|
21
|
-
return false;
|
|
22
|
-
if (/--/.test(name) || /__/.test(name)) return false;
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 解析 YAML frontmatter
|
|
28
|
-
*/
|
|
29
|
-
function parseFrontmatter(content) {
|
|
30
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
31
|
-
if (!match) return null;
|
|
32
|
-
|
|
33
|
-
const frontmatter = {};
|
|
34
|
-
const lines = match[1].split('\n');
|
|
35
|
-
let currentKey = null;
|
|
36
|
-
|
|
37
|
-
for (const line of lines) {
|
|
38
|
-
// 处理 metadata 嵌套
|
|
39
|
-
if (currentKey === 'metadata') {
|
|
40
|
-
if (line.match(/^ {2}[a-zA-Z]/)) {
|
|
41
|
-
const colonIndex = line.indexOf(':');
|
|
42
|
-
if (colonIndex > 0) {
|
|
43
|
-
const key = line.substring(2, colonIndex).trim();
|
|
44
|
-
const value = line
|
|
45
|
-
.substring(colonIndex + 1)
|
|
46
|
-
.trim()
|
|
47
|
-
.replace(/^["']|["']$/g, '');
|
|
48
|
-
frontmatter[currentKey][key] = value;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
} else if (line.match(/^[a-zA-Z]/)) {
|
|
52
|
-
currentKey = null;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const colonIndex = line.indexOf(':');
|
|
58
|
-
if (colonIndex === -1) continue;
|
|
59
|
-
|
|
60
|
-
const key = line.substring(0, colonIndex).trim();
|
|
61
|
-
let value = line.substring(colonIndex + 1).trim();
|
|
62
|
-
|
|
63
|
-
// 移除引号
|
|
64
|
-
if (
|
|
65
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
66
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
67
|
-
) {
|
|
68
|
-
value = value.slice(1, -1);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (key === 'metadata') {
|
|
72
|
-
currentKey = 'metadata';
|
|
73
|
-
frontmatter.metadata = {};
|
|
74
|
-
} else if (key === 'allowed-tools') {
|
|
75
|
-
frontmatter[key] = value
|
|
76
|
-
.split(',')
|
|
77
|
-
.map((t) => t.trim().replace(/^["']|["']$/g, ''))
|
|
78
|
-
.filter((t) => t);
|
|
79
|
-
} else {
|
|
80
|
-
frontmatter[key] = value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return frontmatter;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* 移除 frontmatter,获取正文
|
|
89
|
-
*/
|
|
90
|
-
function stripFrontmatter(content) {
|
|
91
|
-
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
92
|
-
return match ? content.slice(match[0].length).trim() : content.trim();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Skill 元数据
|
|
97
|
-
*/
|
|
98
|
-
class SkillMetadata {
|
|
99
|
-
constructor(data) {
|
|
100
|
-
this.name = data.name || '';
|
|
101
|
-
this.description = data.description || '';
|
|
102
|
-
this.license = data.license || null;
|
|
103
|
-
this.compatibility = data.compatibility || null;
|
|
104
|
-
this.metadata = data.metadata || {};
|
|
105
|
-
this.allowedTools = data.allowedTools || [];
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Skill 类
|
|
111
|
-
*/
|
|
112
|
-
class Skill {
|
|
113
|
-
constructor(metadata, content) {
|
|
114
|
-
this.metadata = metadata;
|
|
115
|
-
this.content = content;
|
|
116
|
-
this._framework = null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* 安装 skill
|
|
121
|
-
*/
|
|
122
|
-
install(framework) {
|
|
123
|
-
this._framework = framework;
|
|
124
|
-
return this;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 获取工具定义
|
|
129
|
-
*/
|
|
130
|
-
getTools() {
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* SkillManager 插件
|
|
137
|
-
*/
|
|
138
|
-
class SkillManagerPlugin extends Plugin {
|
|
139
|
-
constructor(config = {}) {
|
|
140
|
-
super();
|
|
141
|
-
this.name = 'skill-manager';
|
|
142
|
-
this.version = '1.0.0';
|
|
143
|
-
this.description = '技能管理器,加载和管理 Skill';
|
|
144
|
-
this.priority = 5;
|
|
145
|
-
this.system = true;
|
|
146
|
-
|
|
147
|
-
this._framework = null;
|
|
148
|
-
this._skillsDirs = Array.isArray(config.skillsDirs)
|
|
149
|
-
? config.skillsDirs
|
|
150
|
-
: [config.skillsDir || '.agent/skills', 'skills'];
|
|
151
|
-
this._skills = new Map();
|
|
152
|
-
this._loaded = false;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
install(framework) {
|
|
156
|
-
this._framework = framework;
|
|
157
|
-
return this;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
start(framework) {
|
|
161
|
-
this._loadAllSkills();
|
|
162
|
-
|
|
163
|
-
// 注册 loadSkill 工具
|
|
164
|
-
framework.registerTool({
|
|
165
|
-
name: 'loadSkill',
|
|
166
|
-
description: '加载指定技能,获取技能的使用指南和内容',
|
|
167
|
-
inputSchema: z.object({
|
|
168
|
-
skill: z.string().describe('技能名称'),
|
|
169
|
-
}),
|
|
170
|
-
execute: async (args) => {
|
|
171
|
-
const skillName = args.skill;
|
|
172
|
-
const skill = this.getSkill(skillName);
|
|
173
|
-
if (!skill) {
|
|
174
|
-
return { success: false, error: `Skill '${skillName}' not found` };
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
success: true,
|
|
178
|
-
name: skill.name,
|
|
179
|
-
description: skill.metadata?.description || '',
|
|
180
|
-
content: skill.content,
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// 注册 reloadSkills 工具
|
|
186
|
-
framework.registerTool({
|
|
187
|
-
name: 'reloadSkills',
|
|
188
|
-
description: '重载所有技能,当用户添加新技能或修改技能后调用此工具',
|
|
189
|
-
inputSchema: z.object({}),
|
|
190
|
-
execute: async () => {
|
|
191
|
-
this.reload(this._framework);
|
|
192
|
-
return {
|
|
193
|
-
success: true,
|
|
194
|
-
message: `Skills reloaded. Total: ${this._skills.size}`,
|
|
195
|
-
skills: Array.from(this._skills.keys()),
|
|
196
|
-
};
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// 注册 loadReference 工具(按需加载 skill 的附加文档)
|
|
201
|
-
framework.registerTool({
|
|
202
|
-
name: 'loadReference',
|
|
203
|
-
description: '加载指定技能的附加参考文档(references 目录下的文件)',
|
|
204
|
-
inputSchema: z.object({
|
|
205
|
-
skill: z.string().describe('技能名称'),
|
|
206
|
-
reference: z.string().describe('参考文档名称(不含 .md 后缀)'),
|
|
207
|
-
list: z.boolean().optional().describe('如果为 true,列出该技能的所有可用的 reference 文件'),
|
|
208
|
-
}),
|
|
209
|
-
execute: async (args) => {
|
|
210
|
-
if (args.list) {
|
|
211
|
-
// 列出该技能的所有 reference
|
|
212
|
-
const refs = this.listReferences(args.skill);
|
|
213
|
-
return {
|
|
214
|
-
success: true,
|
|
215
|
-
skill: args.skill,
|
|
216
|
-
references: refs,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const content = this.loadReference(args.skill, args.reference);
|
|
221
|
-
if (content === null) {
|
|
222
|
-
return {
|
|
223
|
-
success: false,
|
|
224
|
-
error: `Reference '${args.reference}' not found in skill '${args.skill}'`,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
return {
|
|
228
|
-
success: true,
|
|
229
|
-
skill: args.skill,
|
|
230
|
-
reference: args.reference,
|
|
231
|
-
content,
|
|
232
|
-
};
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// 注册 listScripts 工具(列出 skill 的脚本)
|
|
237
|
-
framework.registerTool({
|
|
238
|
-
name: 'listScripts',
|
|
239
|
-
description: '列出指定技能下的所有可执行脚本',
|
|
240
|
-
inputSchema: z.object({
|
|
241
|
-
skill: z.string().describe('技能名称'),
|
|
242
|
-
}),
|
|
243
|
-
execute: async (args) => {
|
|
244
|
-
const scripts = this.listScripts(args.skill);
|
|
245
|
-
return {
|
|
246
|
-
success: true,
|
|
247
|
-
skill: args.skill,
|
|
248
|
-
scripts,
|
|
249
|
-
};
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// 注册 loadScript 工具(读取脚本内容)
|
|
254
|
-
framework.registerTool({
|
|
255
|
-
name: 'loadScript',
|
|
256
|
-
description: '读取指定技能下脚本文件的内容',
|
|
257
|
-
inputSchema: z.object({
|
|
258
|
-
skill: z.string().describe('技能名称'),
|
|
259
|
-
script: z.string().describe('脚本名称(包含扩展名)'),
|
|
260
|
-
}),
|
|
261
|
-
execute: async (args) => {
|
|
262
|
-
const content = this.loadScript(args.skill, args.script);
|
|
263
|
-
if (content === null) {
|
|
264
|
-
return {
|
|
265
|
-
success: false,
|
|
266
|
-
error: `Script '${args.script}' not found in skill '${args.skill}'`,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
success: true,
|
|
271
|
-
skill: args.skill,
|
|
272
|
-
script: args.script,
|
|
273
|
-
content,
|
|
274
|
-
};
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
return this;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* 加载所有 skills
|
|
283
|
-
*/
|
|
284
|
-
_loadAllSkills() {
|
|
285
|
-
if (this._loaded) return;
|
|
286
|
-
|
|
287
|
-
let totalLoaded = 0;
|
|
288
|
-
|
|
289
|
-
for (const skillsDir of this._skillsDirs) {
|
|
290
|
-
const resolvedDir = path.resolve(process.cwd(), skillsDir);
|
|
291
|
-
|
|
292
|
-
if (!fs.existsSync(resolvedDir)) {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
|
|
298
|
-
|
|
299
|
-
for (const entry of entries) {
|
|
300
|
-
if (!entry.isDirectory()) continue;
|
|
301
|
-
|
|
302
|
-
const skillPath = path.join(resolvedDir, entry.name);
|
|
303
|
-
|
|
304
|
-
if (!isValidSkillName(entry.name)) {
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// 跳过已加载的 skill
|
|
309
|
-
if (this._skills.has(entry.name)) {
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
this._loadSkill(entry.name, skillPath);
|
|
315
|
-
totalLoaded++;
|
|
316
|
-
} catch (err) {
|
|
317
|
-
log.error(` Failed to load skill '${entry.name}':`, err.message);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
} catch (err) {
|
|
321
|
-
log.error('Failed to load skills from:', skillsDir, err.message);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
log.info(` Loaded ${this._skills.size} skills`);
|
|
326
|
-
this._loaded = true;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* 递归查找 SKILL.md 或 AGENTS.md(支持多层嵌套目录)
|
|
331
|
-
* @param {string} dir - 要搜索的目录
|
|
332
|
-
* @returns {string|null} 找到的 markdown 文件路径
|
|
333
|
-
*/
|
|
334
|
-
_findSkillMarkdown(dir) {
|
|
335
|
-
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
340
|
-
|
|
341
|
-
// 第一遍:优先查找 SKILL.MD
|
|
342
|
-
for (const entry of entries) {
|
|
343
|
-
if (entry.isFile() && entry.name.toUpperCase() === 'SKILL.MD') {
|
|
344
|
-
return path.join(dir, entry.name);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// 第二遍:查找 AGENTS.MD
|
|
349
|
-
for (const entry of entries) {
|
|
350
|
-
if (entry.isFile() && entry.name.toUpperCase() === 'AGENTS.MD') {
|
|
351
|
-
return path.join(dir, entry.name);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// 如果没找到,递归搜索子目录(排除 node_modules)
|
|
356
|
-
for (const entry of entries) {
|
|
357
|
-
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
358
|
-
const subPath = path.join(dir, entry.name);
|
|
359
|
-
const found = this._findSkillMarkdown(subPath);
|
|
360
|
-
if (found) return found;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* 加载单个 skill
|
|
369
|
-
*/
|
|
370
|
-
_loadSkill(name, skillPath) {
|
|
371
|
-
// 查找 markdown 文件,优先加载 SKILL.md 或 AGENTS.md(支持多层嵌套)
|
|
372
|
-
const skillMdPath = this._findSkillMarkdown(skillPath);
|
|
373
|
-
if (!skillMdPath) {
|
|
374
|
-
throw new Error('No markdown file found');
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const mainFile = skillMdPath;
|
|
378
|
-
const content = fs.readFileSync(mainFile, 'utf-8');
|
|
379
|
-
const frontmatter = parseFrontmatter(content);
|
|
380
|
-
|
|
381
|
-
if (!frontmatter) {
|
|
382
|
-
throw new Error('No valid frontmatter found');
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const metadata = new SkillMetadata({
|
|
386
|
-
name: frontmatter.name || name,
|
|
387
|
-
description: frontmatter.description || '',
|
|
388
|
-
license: frontmatter.license,
|
|
389
|
-
compatibility: frontmatter.compatibility,
|
|
390
|
-
metadata: frontmatter.metadata || {},
|
|
391
|
-
allowedTools: frontmatter['allowed-tools'] || [],
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const skill = new Skill(metadata, stripFrontmatter(content));
|
|
395
|
-
skill.install(this._framework);
|
|
396
|
-
|
|
397
|
-
// 扫描 references 子目录(按需加载的附加文档)
|
|
398
|
-
const references = this._scanReferences(skillPath);
|
|
399
|
-
|
|
400
|
-
// 扫描 scripts 子目录(可执行脚本)
|
|
401
|
-
const scripts = this._scanScripts(skillPath);
|
|
402
|
-
|
|
403
|
-
this._skills.set(name, {
|
|
404
|
-
name,
|
|
405
|
-
metadata,
|
|
406
|
-
content: skill.content,
|
|
407
|
-
instance: skill,
|
|
408
|
-
path: skillPath,
|
|
409
|
-
references,
|
|
410
|
-
scripts,
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
const refsInfo = references.size > 0 ? `, ${references.size} refs` : '';
|
|
414
|
-
const scriptsInfo = scripts.size > 0 ? `, ${scripts.size} scripts` : '';
|
|
415
|
-
//log.info(` Loaded skill: ${name}${refsInfo}${scriptsInfo}`)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* 扫描 references 子目录,按需加载
|
|
420
|
-
* @param {string} skillPath - skill 目录路径
|
|
421
|
-
* @returns {Map} references 文件映射 { filename: { path, content } }
|
|
422
|
-
*/
|
|
423
|
-
_scanReferences(skillPath) {
|
|
424
|
-
const references = new Map();
|
|
425
|
-
const refsDir = path.join(skillPath, 'references');
|
|
426
|
-
|
|
427
|
-
if (!fs.existsSync(refsDir) || !fs.statSync(refsDir).isDirectory()) {
|
|
428
|
-
return references;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
try {
|
|
432
|
-
const entries = fs.readdirSync(refsDir, { withFileTypes: true });
|
|
433
|
-
for (const entry of entries) {
|
|
434
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
435
|
-
const refPath = path.join(refsDir, entry.name);
|
|
436
|
-
const refName = entry.name.replace('.md', '');
|
|
437
|
-
references.set(refName, {
|
|
438
|
-
path: refPath,
|
|
439
|
-
content: null, // 延迟加载
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
} catch (err) {
|
|
444
|
-
log.warn(` Failed to scan references for ${skillPath}:`, err.message);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return references;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* 扫描 scripts 子目录,获取脚本列表
|
|
452
|
-
* @param {string} skillPath - skill 目录路径
|
|
453
|
-
* @returns {Map} scripts 映射 { filename: { path, isExecutable } }
|
|
454
|
-
*/
|
|
455
|
-
_scanScripts(skillPath) {
|
|
456
|
-
const scripts = new Map();
|
|
457
|
-
const scriptsDir = path.join(skillPath, 'scripts');
|
|
458
|
-
|
|
459
|
-
if (!fs.existsSync(scriptsDir) || !fs.statSync(scriptsDir).isDirectory()) {
|
|
460
|
-
return scripts;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
const entries = fs.readdirSync(scriptsDir, { withFileTypes: true });
|
|
465
|
-
for (const entry of entries) {
|
|
466
|
-
if (entry.isFile()) {
|
|
467
|
-
const scriptPath = path.join(scriptsDir, entry.name);
|
|
468
|
-
// 检查文件是否有执行权限(或检查扩展名)
|
|
469
|
-
const isExecutable =
|
|
470
|
-
entry.name.endsWith('.sh') ||
|
|
471
|
-
entry.name.endsWith('.js') ||
|
|
472
|
-
entry.name.endsWith('.py') ||
|
|
473
|
-
entry.name.endsWith('.ts');
|
|
474
|
-
scripts.set(entry.name, {
|
|
475
|
-
path: scriptPath,
|
|
476
|
-
isExecutable,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
} catch (err) {
|
|
481
|
-
log.warn(` Failed to scan scripts for ${skillPath}:`, err.message);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
return scripts;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* 按需加载 reference 文件
|
|
489
|
-
* @param {string} skillName - skill 名称
|
|
490
|
-
* @param {string} refName - reference 文件名(不含 .md)
|
|
491
|
-
* @returns {string|null} 文件内容
|
|
492
|
-
*/
|
|
493
|
-
loadReference(skillName, refName) {
|
|
494
|
-
const skill = this._skills.get(skillName);
|
|
495
|
-
if (!skill || !skill.references.has(refName)) {
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const ref = skill.references.get(refName);
|
|
500
|
-
if (!ref.content) {
|
|
501
|
-
try {
|
|
502
|
-
ref.content = fs.readFileSync(ref.path, 'utf-8');
|
|
503
|
-
} catch (err) {
|
|
504
|
-
log.error(` Failed to load reference ${skillName}/${refName}:`, err.message);
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return ref.content;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* 获取 skill 的 reference 列表
|
|
514
|
-
* @param {string} skillName
|
|
515
|
-
* @returns {string[]} reference 文件名列表
|
|
516
|
-
*/
|
|
517
|
-
listReferences(skillName) {
|
|
518
|
-
const skill = this._skills.get(skillName);
|
|
519
|
-
if (!skill) return [];
|
|
520
|
-
return Array.from(skill.references.keys());
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* 获取 skill 的 scripts 列表
|
|
525
|
-
* @param {string} skillName
|
|
526
|
-
* @returns {Object[]} script 信息列表 [{ name, path, isExecutable }]
|
|
527
|
-
*/
|
|
528
|
-
listScripts(skillName) {
|
|
529
|
-
const skill = this._skills.get(skillName);
|
|
530
|
-
if (!skill) return [];
|
|
531
|
-
return Array.from(skill.scripts.entries()).map(([name, info]) => ({
|
|
532
|
-
name,
|
|
533
|
-
path: info.path,
|
|
534
|
-
isExecutable: info.isExecutable,
|
|
535
|
-
}));
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* 读取脚本内容
|
|
540
|
-
* @param {string} skillName
|
|
541
|
-
* @param {string} scriptName
|
|
542
|
-
* @returns {string|null}
|
|
543
|
-
*/
|
|
544
|
-
loadScript(skillName, scriptName) {
|
|
545
|
-
const skill = this._skills.get(skillName);
|
|
546
|
-
if (!skill || !skill.scripts.has(scriptName)) {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const script = skill.scripts.get(scriptName);
|
|
551
|
-
try {
|
|
552
|
-
return fs.readFileSync(script.path, 'utf-8');
|
|
553
|
-
} catch (err) {
|
|
554
|
-
log.error(` Failed to load script ${skillName}/${scriptName}:`, err.message);
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* 获取所有 skills
|
|
561
|
-
*/
|
|
562
|
-
getAllSkills() {
|
|
563
|
-
return Array.from(this._skills.values());
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* 获取 skill
|
|
568
|
-
*/
|
|
569
|
-
getSkill(name) {
|
|
570
|
-
return this._skills.get(name);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* 检查 skill 是否存在
|
|
575
|
-
*/
|
|
576
|
-
hasSkill(name) {
|
|
577
|
-
return this._skills.has(name);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* 获取 skill 数量
|
|
582
|
-
*/
|
|
583
|
-
size() {
|
|
584
|
-
return this._skills.size;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
reload(framework) {
|
|
588
|
-
log.info(' Reloading...');
|
|
589
|
-
this._skills.clear();
|
|
590
|
-
this._loaded = false;
|
|
591
|
-
this._framework = framework;
|
|
592
|
-
this._loadAllSkills();
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
uninstall(framework) {
|
|
596
|
-
this._skills.clear();
|
|
597
|
-
this._framework = null;
|
|
598
|
-
this._loaded = false;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
module.exports = {
|
|
603
|
-
SkillManagerPlugin,
|
|
604
|
-
Skill,
|
|
605
|
-
SkillMetadata,
|
|
606
|
-
isValidSkillName,
|
|
607
|
-
parseFrontmatter,
|
|
608
|
-
stripFrontmatter,
|
|
609
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* SkillManager 技能管理器
|
|
3
|
+
* 加载和管理 Skill
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { Plugin } = require('../core/plugin-base');
|
|
9
|
+
const { logger } = require('../utils/logger');
|
|
10
|
+
const log = logger.child('SkillManager');
|
|
11
|
+
const { z } = require('zod');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 验证 skill 名称
|
|
15
|
+
* 1-64字符,字母数字、下划线和连字符,不能以连字符或下划线开头或结尾
|
|
16
|
+
*/
|
|
17
|
+
function isValidSkillName(name) {
|
|
18
|
+
if (!name || name.length < 1 || name.length > 64) return false;
|
|
19
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) return false;
|
|
20
|
+
if (name.startsWith('-') || name.startsWith('_') || name.endsWith('-') || name.endsWith('_'))
|
|
21
|
+
return false;
|
|
22
|
+
if (/--/.test(name) || /__/.test(name)) return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 解析 YAML frontmatter
|
|
28
|
+
*/
|
|
29
|
+
function parseFrontmatter(content) {
|
|
30
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
31
|
+
if (!match) return null;
|
|
32
|
+
|
|
33
|
+
const frontmatter = {};
|
|
34
|
+
const lines = match[1].split('\n');
|
|
35
|
+
let currentKey = null;
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
// 处理 metadata 嵌套
|
|
39
|
+
if (currentKey === 'metadata') {
|
|
40
|
+
if (line.match(/^ {2}[a-zA-Z]/)) {
|
|
41
|
+
const colonIndex = line.indexOf(':');
|
|
42
|
+
if (colonIndex > 0) {
|
|
43
|
+
const key = line.substring(2, colonIndex).trim();
|
|
44
|
+
const value = line
|
|
45
|
+
.substring(colonIndex + 1)
|
|
46
|
+
.trim()
|
|
47
|
+
.replace(/^["']|["']$/g, '');
|
|
48
|
+
frontmatter[currentKey][key] = value;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
} else if (line.match(/^[a-zA-Z]/)) {
|
|
52
|
+
currentKey = null;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const colonIndex = line.indexOf(':');
|
|
58
|
+
if (colonIndex === -1) continue;
|
|
59
|
+
|
|
60
|
+
const key = line.substring(0, colonIndex).trim();
|
|
61
|
+
let value = line.substring(colonIndex + 1).trim();
|
|
62
|
+
|
|
63
|
+
// 移除引号
|
|
64
|
+
if (
|
|
65
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
66
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
67
|
+
) {
|
|
68
|
+
value = value.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (key === 'metadata') {
|
|
72
|
+
currentKey = 'metadata';
|
|
73
|
+
frontmatter.metadata = {};
|
|
74
|
+
} else if (key === 'allowed-tools') {
|
|
75
|
+
frontmatter[key] = value
|
|
76
|
+
.split(',')
|
|
77
|
+
.map((t) => t.trim().replace(/^["']|["']$/g, ''))
|
|
78
|
+
.filter((t) => t);
|
|
79
|
+
} else {
|
|
80
|
+
frontmatter[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return frontmatter;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 移除 frontmatter,获取正文
|
|
89
|
+
*/
|
|
90
|
+
function stripFrontmatter(content) {
|
|
91
|
+
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
92
|
+
return match ? content.slice(match[0].length).trim() : content.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Skill 元数据
|
|
97
|
+
*/
|
|
98
|
+
class SkillMetadata {
|
|
99
|
+
constructor(data) {
|
|
100
|
+
this.name = data.name || '';
|
|
101
|
+
this.description = data.description || '';
|
|
102
|
+
this.license = data.license || null;
|
|
103
|
+
this.compatibility = data.compatibility || null;
|
|
104
|
+
this.metadata = data.metadata || {};
|
|
105
|
+
this.allowedTools = data.allowedTools || [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Skill 类
|
|
111
|
+
*/
|
|
112
|
+
class Skill {
|
|
113
|
+
constructor(metadata, content) {
|
|
114
|
+
this.metadata = metadata;
|
|
115
|
+
this.content = content;
|
|
116
|
+
this._framework = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 安装 skill
|
|
121
|
+
*/
|
|
122
|
+
install(framework) {
|
|
123
|
+
this._framework = framework;
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 获取工具定义
|
|
129
|
+
*/
|
|
130
|
+
getTools() {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* SkillManager 插件
|
|
137
|
+
*/
|
|
138
|
+
class SkillManagerPlugin extends Plugin {
|
|
139
|
+
constructor(config = {}) {
|
|
140
|
+
super();
|
|
141
|
+
this.name = 'skill-manager';
|
|
142
|
+
this.version = '1.0.0';
|
|
143
|
+
this.description = '技能管理器,加载和管理 Skill';
|
|
144
|
+
this.priority = 5;
|
|
145
|
+
this.system = true;
|
|
146
|
+
|
|
147
|
+
this._framework = null;
|
|
148
|
+
this._skillsDirs = Array.isArray(config.skillsDirs)
|
|
149
|
+
? config.skillsDirs
|
|
150
|
+
: [config.skillsDir || '.agent/skills', 'skills'];
|
|
151
|
+
this._skills = new Map();
|
|
152
|
+
this._loaded = false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
install(framework) {
|
|
156
|
+
this._framework = framework;
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
start(framework) {
|
|
161
|
+
this._loadAllSkills();
|
|
162
|
+
|
|
163
|
+
// 注册 loadSkill 工具
|
|
164
|
+
framework.registerTool({
|
|
165
|
+
name: 'loadSkill',
|
|
166
|
+
description: '加载指定技能,获取技能的使用指南和内容',
|
|
167
|
+
inputSchema: z.object({
|
|
168
|
+
skill: z.string().describe('技能名称'),
|
|
169
|
+
}),
|
|
170
|
+
execute: async (args) => {
|
|
171
|
+
const skillName = args.skill;
|
|
172
|
+
const skill = this.getSkill(skillName);
|
|
173
|
+
if (!skill) {
|
|
174
|
+
return { success: false, error: `Skill '${skillName}' not found` };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
name: skill.name,
|
|
179
|
+
description: skill.metadata?.description || '',
|
|
180
|
+
content: skill.content,
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// 注册 reloadSkills 工具
|
|
186
|
+
framework.registerTool({
|
|
187
|
+
name: 'reloadSkills',
|
|
188
|
+
description: '重载所有技能,当用户添加新技能或修改技能后调用此工具',
|
|
189
|
+
inputSchema: z.object({}),
|
|
190
|
+
execute: async () => {
|
|
191
|
+
this.reload(this._framework);
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
message: `Skills reloaded. Total: ${this._skills.size}`,
|
|
195
|
+
skills: Array.from(this._skills.keys()),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// 注册 loadReference 工具(按需加载 skill 的附加文档)
|
|
201
|
+
framework.registerTool({
|
|
202
|
+
name: 'loadReference',
|
|
203
|
+
description: '加载指定技能的附加参考文档(references 目录下的文件)',
|
|
204
|
+
inputSchema: z.object({
|
|
205
|
+
skill: z.string().describe('技能名称'),
|
|
206
|
+
reference: z.string().describe('参考文档名称(不含 .md 后缀)'),
|
|
207
|
+
list: z.boolean().optional().describe('如果为 true,列出该技能的所有可用的 reference 文件'),
|
|
208
|
+
}),
|
|
209
|
+
execute: async (args) => {
|
|
210
|
+
if (args.list) {
|
|
211
|
+
// 列出该技能的所有 reference
|
|
212
|
+
const refs = this.listReferences(args.skill);
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
skill: args.skill,
|
|
216
|
+
references: refs,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const content = this.loadReference(args.skill, args.reference);
|
|
221
|
+
if (content === null) {
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
error: `Reference '${args.reference}' not found in skill '${args.skill}'`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
skill: args.skill,
|
|
230
|
+
reference: args.reference,
|
|
231
|
+
content,
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// 注册 listScripts 工具(列出 skill 的脚本)
|
|
237
|
+
framework.registerTool({
|
|
238
|
+
name: 'listScripts',
|
|
239
|
+
description: '列出指定技能下的所有可执行脚本',
|
|
240
|
+
inputSchema: z.object({
|
|
241
|
+
skill: z.string().describe('技能名称'),
|
|
242
|
+
}),
|
|
243
|
+
execute: async (args) => {
|
|
244
|
+
const scripts = this.listScripts(args.skill);
|
|
245
|
+
return {
|
|
246
|
+
success: true,
|
|
247
|
+
skill: args.skill,
|
|
248
|
+
scripts,
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// 注册 loadScript 工具(读取脚本内容)
|
|
254
|
+
framework.registerTool({
|
|
255
|
+
name: 'loadScript',
|
|
256
|
+
description: '读取指定技能下脚本文件的内容',
|
|
257
|
+
inputSchema: z.object({
|
|
258
|
+
skill: z.string().describe('技能名称'),
|
|
259
|
+
script: z.string().describe('脚本名称(包含扩展名)'),
|
|
260
|
+
}),
|
|
261
|
+
execute: async (args) => {
|
|
262
|
+
const content = this.loadScript(args.skill, args.script);
|
|
263
|
+
if (content === null) {
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
error: `Script '${args.script}' not found in skill '${args.skill}'`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
success: true,
|
|
271
|
+
skill: args.skill,
|
|
272
|
+
script: args.script,
|
|
273
|
+
content,
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 加载所有 skills
|
|
283
|
+
*/
|
|
284
|
+
_loadAllSkills() {
|
|
285
|
+
if (this._loaded) return;
|
|
286
|
+
|
|
287
|
+
let totalLoaded = 0;
|
|
288
|
+
|
|
289
|
+
for (const skillsDir of this._skillsDirs) {
|
|
290
|
+
const resolvedDir = path.resolve(process.cwd(), skillsDir);
|
|
291
|
+
|
|
292
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
|
|
298
|
+
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
if (!entry.isDirectory()) continue;
|
|
301
|
+
|
|
302
|
+
const skillPath = path.join(resolvedDir, entry.name);
|
|
303
|
+
|
|
304
|
+
if (!isValidSkillName(entry.name)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 跳过已加载的 skill
|
|
309
|
+
if (this._skills.has(entry.name)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
this._loadSkill(entry.name, skillPath);
|
|
315
|
+
totalLoaded++;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
log.error(` Failed to load skill '${entry.name}':`, err.message);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
log.error('Failed to load skills from:', skillsDir, err.message);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
log.info(` Loaded ${this._skills.size} skills`);
|
|
326
|
+
this._loaded = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 递归查找 SKILL.md 或 AGENTS.md(支持多层嵌套目录)
|
|
331
|
+
* @param {string} dir - 要搜索的目录
|
|
332
|
+
* @returns {string|null} 找到的 markdown 文件路径
|
|
333
|
+
*/
|
|
334
|
+
_findSkillMarkdown(dir) {
|
|
335
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
340
|
+
|
|
341
|
+
// 第一遍:优先查找 SKILL.MD
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
if (entry.isFile() && entry.name.toUpperCase() === 'SKILL.MD') {
|
|
344
|
+
return path.join(dir, entry.name);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 第二遍:查找 AGENTS.MD
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
if (entry.isFile() && entry.name.toUpperCase() === 'AGENTS.MD') {
|
|
351
|
+
return path.join(dir, entry.name);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 如果没找到,递归搜索子目录(排除 node_modules)
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
358
|
+
const subPath = path.join(dir, entry.name);
|
|
359
|
+
const found = this._findSkillMarkdown(subPath);
|
|
360
|
+
if (found) return found;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 加载单个 skill
|
|
369
|
+
*/
|
|
370
|
+
_loadSkill(name, skillPath) {
|
|
371
|
+
// 查找 markdown 文件,优先加载 SKILL.md 或 AGENTS.md(支持多层嵌套)
|
|
372
|
+
const skillMdPath = this._findSkillMarkdown(skillPath);
|
|
373
|
+
if (!skillMdPath) {
|
|
374
|
+
throw new Error('No markdown file found');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const mainFile = skillMdPath;
|
|
378
|
+
const content = fs.readFileSync(mainFile, 'utf-8');
|
|
379
|
+
const frontmatter = parseFrontmatter(content);
|
|
380
|
+
|
|
381
|
+
if (!frontmatter) {
|
|
382
|
+
throw new Error('No valid frontmatter found');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const metadata = new SkillMetadata({
|
|
386
|
+
name: frontmatter.name || name,
|
|
387
|
+
description: frontmatter.description || '',
|
|
388
|
+
license: frontmatter.license,
|
|
389
|
+
compatibility: frontmatter.compatibility,
|
|
390
|
+
metadata: frontmatter.metadata || {},
|
|
391
|
+
allowedTools: frontmatter['allowed-tools'] || [],
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const skill = new Skill(metadata, stripFrontmatter(content));
|
|
395
|
+
skill.install(this._framework);
|
|
396
|
+
|
|
397
|
+
// 扫描 references 子目录(按需加载的附加文档)
|
|
398
|
+
const references = this._scanReferences(skillPath);
|
|
399
|
+
|
|
400
|
+
// 扫描 scripts 子目录(可执行脚本)
|
|
401
|
+
const scripts = this._scanScripts(skillPath);
|
|
402
|
+
|
|
403
|
+
this._skills.set(name, {
|
|
404
|
+
name,
|
|
405
|
+
metadata,
|
|
406
|
+
content: skill.content,
|
|
407
|
+
instance: skill,
|
|
408
|
+
path: skillPath,
|
|
409
|
+
references,
|
|
410
|
+
scripts,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const refsInfo = references.size > 0 ? `, ${references.size} refs` : '';
|
|
414
|
+
const scriptsInfo = scripts.size > 0 ? `, ${scripts.size} scripts` : '';
|
|
415
|
+
//log.info(` Loaded skill: ${name}${refsInfo}${scriptsInfo}`)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* 扫描 references 子目录,按需加载
|
|
420
|
+
* @param {string} skillPath - skill 目录路径
|
|
421
|
+
* @returns {Map} references 文件映射 { filename: { path, content } }
|
|
422
|
+
*/
|
|
423
|
+
_scanReferences(skillPath) {
|
|
424
|
+
const references = new Map();
|
|
425
|
+
const refsDir = path.join(skillPath, 'references');
|
|
426
|
+
|
|
427
|
+
if (!fs.existsSync(refsDir) || !fs.statSync(refsDir).isDirectory()) {
|
|
428
|
+
return references;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const entries = fs.readdirSync(refsDir, { withFileTypes: true });
|
|
433
|
+
for (const entry of entries) {
|
|
434
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
435
|
+
const refPath = path.join(refsDir, entry.name);
|
|
436
|
+
const refName = entry.name.replace('.md', '');
|
|
437
|
+
references.set(refName, {
|
|
438
|
+
path: refPath,
|
|
439
|
+
content: null, // 延迟加载
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch (err) {
|
|
444
|
+
log.warn(` Failed to scan references for ${skillPath}:`, err.message);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return references;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* 扫描 scripts 子目录,获取脚本列表
|
|
452
|
+
* @param {string} skillPath - skill 目录路径
|
|
453
|
+
* @returns {Map} scripts 映射 { filename: { path, isExecutable } }
|
|
454
|
+
*/
|
|
455
|
+
_scanScripts(skillPath) {
|
|
456
|
+
const scripts = new Map();
|
|
457
|
+
const scriptsDir = path.join(skillPath, 'scripts');
|
|
458
|
+
|
|
459
|
+
if (!fs.existsSync(scriptsDir) || !fs.statSync(scriptsDir).isDirectory()) {
|
|
460
|
+
return scripts;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const entries = fs.readdirSync(scriptsDir, { withFileTypes: true });
|
|
465
|
+
for (const entry of entries) {
|
|
466
|
+
if (entry.isFile()) {
|
|
467
|
+
const scriptPath = path.join(scriptsDir, entry.name);
|
|
468
|
+
// 检查文件是否有执行权限(或检查扩展名)
|
|
469
|
+
const isExecutable =
|
|
470
|
+
entry.name.endsWith('.sh') ||
|
|
471
|
+
entry.name.endsWith('.js') ||
|
|
472
|
+
entry.name.endsWith('.py') ||
|
|
473
|
+
entry.name.endsWith('.ts');
|
|
474
|
+
scripts.set(entry.name, {
|
|
475
|
+
path: scriptPath,
|
|
476
|
+
isExecutable,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} catch (err) {
|
|
481
|
+
log.warn(` Failed to scan scripts for ${skillPath}:`, err.message);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return scripts;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 按需加载 reference 文件
|
|
489
|
+
* @param {string} skillName - skill 名称
|
|
490
|
+
* @param {string} refName - reference 文件名(不含 .md)
|
|
491
|
+
* @returns {string|null} 文件内容
|
|
492
|
+
*/
|
|
493
|
+
loadReference(skillName, refName) {
|
|
494
|
+
const skill = this._skills.get(skillName);
|
|
495
|
+
if (!skill || !skill.references.has(refName)) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const ref = skill.references.get(refName);
|
|
500
|
+
if (!ref.content) {
|
|
501
|
+
try {
|
|
502
|
+
ref.content = fs.readFileSync(ref.path, 'utf-8');
|
|
503
|
+
} catch (err) {
|
|
504
|
+
log.error(` Failed to load reference ${skillName}/${refName}:`, err.message);
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return ref.content;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 获取 skill 的 reference 列表
|
|
514
|
+
* @param {string} skillName
|
|
515
|
+
* @returns {string[]} reference 文件名列表
|
|
516
|
+
*/
|
|
517
|
+
listReferences(skillName) {
|
|
518
|
+
const skill = this._skills.get(skillName);
|
|
519
|
+
if (!skill) return [];
|
|
520
|
+
return Array.from(skill.references.keys());
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 获取 skill 的 scripts 列表
|
|
525
|
+
* @param {string} skillName
|
|
526
|
+
* @returns {Object[]} script 信息列表 [{ name, path, isExecutable }]
|
|
527
|
+
*/
|
|
528
|
+
listScripts(skillName) {
|
|
529
|
+
const skill = this._skills.get(skillName);
|
|
530
|
+
if (!skill) return [];
|
|
531
|
+
return Array.from(skill.scripts.entries()).map(([name, info]) => ({
|
|
532
|
+
name,
|
|
533
|
+
path: info.path,
|
|
534
|
+
isExecutable: info.isExecutable,
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* 读取脚本内容
|
|
540
|
+
* @param {string} skillName
|
|
541
|
+
* @param {string} scriptName
|
|
542
|
+
* @returns {string|null}
|
|
543
|
+
*/
|
|
544
|
+
loadScript(skillName, scriptName) {
|
|
545
|
+
const skill = this._skills.get(skillName);
|
|
546
|
+
if (!skill || !skill.scripts.has(scriptName)) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const script = skill.scripts.get(scriptName);
|
|
551
|
+
try {
|
|
552
|
+
return fs.readFileSync(script.path, 'utf-8');
|
|
553
|
+
} catch (err) {
|
|
554
|
+
log.error(` Failed to load script ${skillName}/${scriptName}:`, err.message);
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 获取所有 skills
|
|
561
|
+
*/
|
|
562
|
+
getAllSkills() {
|
|
563
|
+
return Array.from(this._skills.values());
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* 获取 skill
|
|
568
|
+
*/
|
|
569
|
+
getSkill(name) {
|
|
570
|
+
return this._skills.get(name);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* 检查 skill 是否存在
|
|
575
|
+
*/
|
|
576
|
+
hasSkill(name) {
|
|
577
|
+
return this._skills.has(name);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 获取 skill 数量
|
|
582
|
+
*/
|
|
583
|
+
size() {
|
|
584
|
+
return this._skills.size;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
reload(framework) {
|
|
588
|
+
log.info(' Reloading...');
|
|
589
|
+
this._skills.clear();
|
|
590
|
+
this._loaded = false;
|
|
591
|
+
this._framework = framework;
|
|
592
|
+
this._loadAllSkills();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
uninstall(framework) {
|
|
596
|
+
this._skills.clear();
|
|
597
|
+
this._framework = null;
|
|
598
|
+
this._loaded = false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
module.exports = {
|
|
603
|
+
SkillManagerPlugin,
|
|
604
|
+
Skill,
|
|
605
|
+
SkillMetadata,
|
|
606
|
+
isValidSkillName,
|
|
607
|
+
parseFrontmatter,
|
|
608
|
+
stripFrontmatter,
|
|
609
|
+
};
|