@cicctencent/agent-core 1.0.17 → 1.0.20

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.
Files changed (71) hide show
  1. package/dist/cjs/agent/compaction.d.ts +26 -0
  2. package/dist/cjs/agent/compaction.d.ts.map +1 -0
  3. package/dist/cjs/agent/compaction.js +95 -0
  4. package/dist/cjs/agent/compaction.js.map +1 -0
  5. package/dist/cjs/agent/engine.d.ts +8 -9
  6. package/dist/cjs/agent/engine.d.ts.map +1 -1
  7. package/dist/cjs/agent/engine.js +37 -75
  8. package/dist/cjs/agent/engine.js.map +1 -1
  9. package/dist/cjs/agent/history-manager.d.ts.map +1 -1
  10. package/dist/cjs/agent/history-manager.js +1 -4
  11. package/dist/cjs/agent/history-manager.js.map +1 -1
  12. package/dist/cjs/agent/types.d.ts +2 -0
  13. package/dist/cjs/agent/types.d.ts.map +1 -1
  14. package/dist/cjs/agent/types.js.map +1 -1
  15. package/dist/cjs/index.d.ts +3 -1
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +9 -4
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/skill/frontmatter.d.ts +18 -0
  20. package/dist/cjs/skill/frontmatter.d.ts.map +1 -0
  21. package/dist/cjs/skill/frontmatter.js +332 -0
  22. package/dist/cjs/skill/frontmatter.js.map +1 -0
  23. package/dist/cjs/skill/manager.d.ts +59 -6
  24. package/dist/cjs/skill/manager.d.ts.map +1 -1
  25. package/dist/cjs/skill/manager.js +391 -83
  26. package/dist/cjs/skill/manager.js.map +1 -1
  27. package/dist/cjs/skill/types.d.ts +30 -0
  28. package/dist/cjs/skill/types.d.ts.map +1 -1
  29. package/dist/cjs/store/file-tool-result-store.d.ts +5 -0
  30. package/dist/cjs/store/file-tool-result-store.d.ts.map +1 -1
  31. package/dist/cjs/store/file-tool-result-store.js +14 -0
  32. package/dist/cjs/store/file-tool-result-store.js.map +1 -1
  33. package/dist/cjs/utils/token-counter.d.ts.map +1 -1
  34. package/dist/cjs/utils/token-counter.js +20 -16
  35. package/dist/cjs/utils/token-counter.js.map +1 -1
  36. package/dist/esm/agent/compaction.d.ts +26 -0
  37. package/dist/esm/agent/compaction.d.ts.map +1 -0
  38. package/dist/esm/agent/compaction.js +91 -0
  39. package/dist/esm/agent/compaction.js.map +1 -0
  40. package/dist/esm/agent/engine.d.ts +8 -9
  41. package/dist/esm/agent/engine.d.ts.map +1 -1
  42. package/dist/esm/agent/engine.js +36 -74
  43. package/dist/esm/agent/engine.js.map +1 -1
  44. package/dist/esm/agent/history-manager.d.ts.map +1 -1
  45. package/dist/esm/agent/history-manager.js +1 -4
  46. package/dist/esm/agent/history-manager.js.map +1 -1
  47. package/dist/esm/agent/types.d.ts +2 -0
  48. package/dist/esm/agent/types.d.ts.map +1 -1
  49. package/dist/esm/agent/types.js.map +1 -1
  50. package/dist/esm/index.d.ts +3 -1
  51. package/dist/esm/index.d.ts.map +1 -1
  52. package/dist/esm/index.js +1 -0
  53. package/dist/esm/index.js.map +1 -1
  54. package/dist/esm/skill/frontmatter.d.ts +18 -0
  55. package/dist/esm/skill/frontmatter.d.ts.map +1 -0
  56. package/dist/esm/skill/frontmatter.js +326 -0
  57. package/dist/esm/skill/frontmatter.js.map +1 -0
  58. package/dist/esm/skill/manager.d.ts +59 -6
  59. package/dist/esm/skill/manager.d.ts.map +1 -1
  60. package/dist/esm/skill/manager.js +391 -83
  61. package/dist/esm/skill/manager.js.map +1 -1
  62. package/dist/esm/skill/types.d.ts +30 -0
  63. package/dist/esm/skill/types.d.ts.map +1 -1
  64. package/dist/esm/store/file-tool-result-store.d.ts +5 -0
  65. package/dist/esm/store/file-tool-result-store.d.ts.map +1 -1
  66. package/dist/esm/store/file-tool-result-store.js +14 -0
  67. package/dist/esm/store/file-tool-result-store.js.map +1 -1
  68. package/dist/esm/utils/token-counter.d.ts.map +1 -1
  69. package/dist/esm/utils/token-counter.js +20 -16
  70. package/dist/esm/utils/token-counter.js.map +1 -1
  71. package/package.json +7 -4
@@ -39,6 +39,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.SkillManager = void 0;
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
+ const frontmatter_ts_1 = require("./frontmatter.js");
43
+ /** 需要透传到 metadata 的标准/已知 frontmatter 字段集合 */
44
+ const KNOWN_FM_KEYS = new Set([
45
+ 'name', 'description', 'tags', 'keywords', 'aliases',
46
+ 'requires', 'required_mcps', 'version', 'author',
47
+ 'referenced_skills', 'referencedSkills',
48
+ 'allowed-tools', 'allowedTools', 'allowed_tools',
49
+ 'license', 'compatibility', 'metadata',
50
+ ]);
42
51
  /** 简单 LRU 缓存(基于 Map 插入顺序) */
43
52
  class LRUCache {
44
53
  cache = new Map();
@@ -90,6 +99,8 @@ class SkillManager {
90
99
  nameIndex = new Map();
91
100
  /** content LRU 缓存(懒加载时按需填充) */
92
101
  contentCache;
102
+ /** 嵌套子包扫描结果缓存:packageDir -> Skill[] */
103
+ nestedSkillsCache = new Map();
93
104
  /** 是否启用懒加载 */
94
105
  lazy;
95
106
  /** 记录加载的目录,用于 reload */
@@ -127,16 +138,47 @@ class SkillManager {
127
138
  : path.resolve(dir);
128
139
  if (!fs.existsSync(resolvedDir))
129
140
  return;
130
- // 递归扫描目录及子目录中的所有 .md 文件
141
+ const isSkippableDir = (name) => name === 'node_modules' || name === '.git' || name === '.hg' || name.startsWith('.');
142
+ // 第一遍:递归识别所有标准目录包(含 SKILL.md 的目录)。
143
+ // 关键:识别到包后【继续向下递归】,因此父包内的子目录若也含 SKILL.md
144
+ // (如 modules/zk-strategy/SKILL.md)会被识别为独立子包,支持任意层级嵌套。
145
+ const packageEntries = [];
146
+ const collectPackages = (currentDir, relId) => {
147
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
148
+ const hasSkillMd = entries.some(e => e.isFile() && e.name.toLowerCase() === 'skill.md');
149
+ if (hasSkillMd) {
150
+ packageEntries.push({ dir: currentDir, relId: relId || path.basename(currentDir) });
151
+ // 继续递归(而非 return),以发现嵌套子包
152
+ }
153
+ for (const entry of entries) {
154
+ if (entry.isDirectory() && !isSkippableDir(entry.name)) {
155
+ const childRel = relId ? `${relId}/${entry.name}` : entry.name;
156
+ collectPackages(path.join(currentDir, entry.name), childRel);
157
+ }
158
+ }
159
+ };
160
+ collectPackages(resolvedDir, '');
161
+ const packageSet = packageEntries.map(p => path.resolve(p.dir));
162
+ // 加载目录包(SKILL.md + 资源目录),relId 作为嵌套 storageId
163
+ for (const { dir, relId } of packageEntries) {
164
+ this.loadPackageDir(dir, relId, source, connectorId);
165
+ }
166
+ // 判断路径是否位于某个包目录内
167
+ const isInPackage = (fp) => {
168
+ const abs = path.resolve(fp);
169
+ return packageSet.some(pkg => abs === pkg || abs.startsWith(pkg + path.sep));
170
+ };
171
+ // 第二遍:递归扫描散落的 .md 文件(跳过任何包目录内的文件)
131
172
  const walkDir = (currentDir) => {
132
173
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
133
174
  const mdFiles = [];
134
175
  for (const entry of entries) {
135
176
  const fullPath = path.join(currentDir, entry.name);
136
177
  if (entry.isDirectory()) {
137
- mdFiles.push(...walkDir(fullPath));
178
+ if (!isInPackage(fullPath))
179
+ mdFiles.push(...walkDir(fullPath));
138
180
  }
139
- else if (entry.isFile() && entry.name.endsWith('.md')) {
181
+ else if (entry.isFile() && entry.name.endsWith('.md') && !isInPackage(fullPath)) {
140
182
  mdFiles.push(fullPath);
141
183
  }
142
184
  }
@@ -170,10 +212,210 @@ class SkillManager {
170
212
  }
171
213
  }
172
214
  }
215
+ /** 扫描目录包的资源清单(scripts/ references/ assets/ skills/),返回相对路径(POSIX 风格) */
216
+ static scanPackageResources(packageDir) {
217
+ const scan = (sub) => {
218
+ const dir = path.join(packageDir, sub);
219
+ if (!fs.existsSync(dir))
220
+ return [];
221
+ const out = [];
222
+ const walk = (d) => {
223
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
224
+ const fp = path.join(d, e.name);
225
+ if (e.isDirectory())
226
+ walk(fp);
227
+ else if (e.isFile())
228
+ out.push(path.relative(packageDir, fp).split(path.sep).join('/'));
229
+ }
230
+ };
231
+ walk(dir);
232
+ return out.sort();
233
+ };
234
+ const resources = {};
235
+ const scripts = scan('scripts');
236
+ const references = scan('references');
237
+ const assets = scan('assets');
238
+ // 兼容:包根目录下除 SKILL.md 外的 .md 文档也视为 references(WorkBuddy 旧格式,如 reference.md)
239
+ try {
240
+ for (const e of fs.readdirSync(packageDir, { withFileTypes: true })) {
241
+ if (e.isFile() && e.name.toLowerCase().endsWith('.md') && e.name.toLowerCase() !== 'skill.md') {
242
+ references.push(e.name);
243
+ }
244
+ }
245
+ }
246
+ catch {
247
+ /* ignore */
248
+ }
249
+ if (scripts.length)
250
+ resources.scripts = scripts;
251
+ if (references.length)
252
+ resources.references = [...new Set(references)].sort();
253
+ if (assets.length)
254
+ resources.assets = assets;
255
+ // 扫描嵌套子包(skills/ 目录下的子目录含 SKILL.md)
256
+ const skillsDir = path.join(packageDir, 'skills');
257
+ if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) {
258
+ const nestedSkills = this.scanNestedPackages(skillsDir);
259
+ if (nestedSkills.length > 0) {
260
+ resources.skills = nestedSkills;
261
+ }
262
+ }
263
+ return resources;
264
+ }
265
+ /** 扫描嵌套子包(skills/ 目录下的子目录含 SKILL.md) */
266
+ static scanNestedPackages(skillsDir) {
267
+ const nestedSkills = [];
268
+ const scanDir = (dir, relPath) => {
269
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
270
+ for (const entry of entries) {
271
+ if (!entry.isDirectory())
272
+ continue;
273
+ const subDir = path.join(dir, entry.name);
274
+ const skillMd = path.join(subDir, 'SKILL.md');
275
+ if (fs.existsSync(skillMd)) {
276
+ // 这是一个嵌套子包
277
+ nestedSkills.push(relPath ? `${relPath}/${entry.name}` : entry.name);
278
+ // 继续递归扫描子包的 skills/ 目录
279
+ const subSkillsDir = path.join(subDir, 'skills');
280
+ if (fs.existsSync(subSkillsDir)) {
281
+ scanDir(subSkillsDir, relPath ? `${relPath}/${entry.name}` : entry.name);
282
+ }
283
+ }
284
+ else {
285
+ // 不是子包,继续向下扫描
286
+ scanDir(subDir, relPath ? `${relPath}/${entry.name}` : entry.name);
287
+ }
288
+ }
289
+ };
290
+ scanDir(skillsDir, '');
291
+ return nestedSkills;
292
+ }
293
+ /**
294
+ * 扫描嵌套子包并返回完整的 Skill 对象列表
295
+ * @param parentPkgDir 父包的物化目录
296
+ * @param parentRuntimeId 父包的运行时 ID
297
+ * @param parentPath 父包的 path
298
+ * @returns 嵌套子包列表
299
+ */
300
+ static scanNestedSkills(parentPkgDir, parentRuntimeId, parentPath) {
301
+ const nestedSkills = [];
302
+ const skillsDir = path.join(parentPkgDir, 'skills');
303
+ if (!fs.existsSync(skillsDir)) {
304
+ return nestedSkills;
305
+ }
306
+ const scanDir = (dir, relPath) => {
307
+ try {
308
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
309
+ for (const entry of entries) {
310
+ if (!entry.isDirectory())
311
+ continue;
312
+ const subDir = path.join(dir, entry.name);
313
+ const skillMd = path.join(subDir, 'SKILL.md');
314
+ if (fs.existsSync(skillMd)) {
315
+ // 这是一个嵌套子包
316
+ const subPath = `${parentPath}/skills/${entry.name}`;
317
+ // 使用 / 作为分隔符,避免与 connectorSkillKey 冲突
318
+ const subRuntimeId = `${parentRuntimeId}/${entry.name}`;
319
+ // 解析 SKILL.md 的 frontmatter
320
+ let name = entry.name;
321
+ let description = '';
322
+ let content = '';
323
+ try {
324
+ const raw = fs.readFileSync(skillMd, 'utf-8');
325
+ const { data, body } = (0, frontmatter_ts_1.parseFrontmatter)(raw);
326
+ name = String(data.name || entry.name);
327
+ description = String(data.description || '');
328
+ content = body.trim();
329
+ }
330
+ catch (error) {
331
+ console.warn(`[SkillManager] 解析嵌套子包 SKILL.md 失败: ${subDir}`, error);
332
+ // 使用默认值,继续处理其他子包
333
+ }
334
+ nestedSkills.push({
335
+ id: subRuntimeId,
336
+ name: name,
337
+ description: description,
338
+ tags: [],
339
+ aliases: [],
340
+ content: content,
341
+ source: 'user',
342
+ packageDir: subDir,
343
+ });
344
+ // 继续递归扫描子包的 skills/ 目录
345
+ const subSkillsDir = path.join(subDir, 'skills');
346
+ if (fs.existsSync(subSkillsDir)) {
347
+ scanDir(subSkillsDir, relPath ? `${relPath}/${entry.name}` : entry.name);
348
+ }
349
+ }
350
+ else {
351
+ // 不是子包,继续向下扫描
352
+ scanDir(subDir, relPath ? `${relPath}/${entry.name}` : entry.name);
353
+ }
354
+ }
355
+ }
356
+ catch (error) {
357
+ console.warn(`[SkillManager] 扫描嵌套子包目录失败: ${dir}`, error);
358
+ }
359
+ };
360
+ scanDir(skillsDir, 'skills');
361
+ return nestedSkills;
362
+ }
363
+ /**
364
+ * 加载单个标准目录包(含 SKILL.md 的目录)。
365
+ * 自动扫描资源目录并注册资源清单。
366
+ * @param relId 相对于加载根目录的嵌套 id(如 `master-investment/modules/zk-strategy`);
367
+ * 无连接器时直接作为 storageId,确保不同层级的同名目录互不覆盖。
368
+ */
369
+ loadPackageDir(packageDir, relId, source, connectorId) {
370
+ const resolvedDir = path.resolve(packageDir);
371
+ const skillMd = path.join(resolvedDir, 'SKILL.md');
372
+ if (!fs.existsSync(skillMd))
373
+ return;
374
+ // 嵌套 id:连接器前缀 + 相对路径(用 '/' 分隔,跨平台统一)
375
+ const storageId = connectorId ? SkillManager.connectorSkillKey(connectorId, relId) : relId;
376
+ const baseId = path.basename(resolvedDir);
377
+ // 父包 id:去掉最后一段(如 `master-investment/modules/zk-strategy` -> `master-investment`)
378
+ const parentId = relId.includes('/') ? relId.substring(0, relId.lastIndexOf('/')) : undefined;
379
+ const resources = SkillManager.scanPackageResources(resolvedDir);
380
+ if (this.lazy) {
381
+ const manifest = this.parseManifest(storageId, skillMd, source, connectorId);
382
+ if (manifest) {
383
+ manifest.baseId = baseId;
384
+ manifest.parentId = parentId;
385
+ this.manifests.set(storageId, manifest);
386
+ this.indexManifest(manifest, storageId);
387
+ this.registerPackageResources(storageId, resources, resolvedDir);
388
+ }
389
+ }
390
+ else {
391
+ const raw = fs.readFileSync(skillMd, 'utf-8');
392
+ const skill = this.parse(storageId, raw, source);
393
+ if (connectorId)
394
+ skill.metadata = { ...skill.metadata, connectorId };
395
+ const manifest = this.toManifest(skill, skillMd);
396
+ manifest.baseId = baseId;
397
+ manifest.parentId = parentId;
398
+ this.manifests.set(storageId, manifest);
399
+ this.indexManifest(manifest, storageId);
400
+ this.contentCache.set(storageId, skill.content);
401
+ this.registerPackageResources(storageId, resources, resolvedDir);
402
+ }
403
+ }
173
404
  /** 手动注册 Skill(全量,含 content) */
174
405
  register(skill) {
175
- this.manifests.set(skill.id, this.toManifest(skill));
176
- this.indexManifest(this.manifests.get(skill.id), skill.id);
406
+ const manifest = this.toManifest(skill);
407
+ // 带 packageDir 的物化包:若未携带 resources 清单,自动扫描目录补全过程资源,
408
+ // 使 list_skill_resources / run_skill_script 能正确枚举脚本与文档(多级嵌套同包各自独立)。
409
+ if (skill.packageDir && !manifest.resources) {
410
+ try {
411
+ manifest.resources = SkillManager.scanPackageResources(skill.packageDir);
412
+ }
413
+ catch {
414
+ /* ignore */
415
+ }
416
+ }
417
+ this.manifests.set(skill.id, manifest);
418
+ this.indexManifest(manifest, skill.id);
177
419
  if (skill.content) {
178
420
  this.contentCache.set(skill.id, skill.content);
179
421
  }
@@ -285,17 +527,95 @@ class SkillManager {
285
527
  lazyMode: this.lazy,
286
528
  };
287
529
  }
530
+ // ---- Agent Skills 规范:资源访问(Layer3 按需加载) ----
531
+ /**
532
+ * 注册 Skill 包的资源清单与根目录(标准目录包形态)。
533
+ * 供管理端注入运行时 Skill 时调用,或 loadFromDir 扫描目录包时自动调用。
534
+ */
535
+ registerPackageResources(skillId, resources, packageDir) {
536
+ const manifest = this.resolveManifest(skillId);
537
+ if (!manifest)
538
+ return;
539
+ manifest.resources = resources;
540
+ manifest.packageDir = packageDir;
541
+ }
542
+ /** 列出 Skill 的资源清单(无则返回 null) */
543
+ listResources(skillIdOrName) {
544
+ const manifest = this.resolveManifest(skillIdOrName);
545
+ return manifest?.resources ?? null;
546
+ }
547
+ /**
548
+ * 按需加载 Skill 资源文件文本内容(references/ assets/ 等)。
549
+ * 仅允许读取 packageDir 内的文件,含路径穿越防护。
550
+ */
551
+ loadResource(skillIdOrName, resourcePath) {
552
+ const abs = this.resolveResourcePathByName(skillIdOrName, resourcePath);
553
+ if (!abs)
554
+ return null;
555
+ try {
556
+ return fs.readFileSync(abs, 'utf-8');
557
+ }
558
+ catch (err) {
559
+ console.warn(`[SkillManager] Failed to load resource "${resourcePath}" for "${skillIdOrName}":`, err?.message);
560
+ return null;
561
+ }
562
+ }
563
+ /** 获取资源文件绝对路径(供执行/二进制读取),含路径穿越防护 */
564
+ resolveResourcePathByName(skillIdOrName, resourcePath) {
565
+ const manifest = this.resolveManifest(skillIdOrName);
566
+ return this.resolveResourcePath(manifest, resourcePath);
567
+ }
568
+ /** 解析脚本绝对路径(scripts/ 下),供脚本执行工具调用 */
569
+ resolveScriptPath(skillIdOrName, scriptRel) {
570
+ const rel = scriptRel.startsWith('scripts/') ? scriptRel : `scripts/${scriptRel}`;
571
+ return this.resolveResourcePath(this.resolveManifest(skillIdOrName), rel);
572
+ }
573
+ /** 内部:解析并校验资源路径,确保位于 packageDir 内(防路径穿越) */
574
+ resolveResourcePath(manifest, resourcePath) {
575
+ if (!manifest?.packageDir || !resourcePath)
576
+ return null;
577
+ const base = path.resolve(manifest.packageDir);
578
+ const abs = path.resolve(base, resourcePath);
579
+ // 必须位于包根目录内
580
+ if (abs !== base && !abs.startsWith(base + path.sep))
581
+ return null;
582
+ if (!fs.existsSync(abs))
583
+ return null;
584
+ return abs;
585
+ }
288
586
  /** 重新加载(文件变更后刷新) */
289
587
  reload() {
290
588
  this.manifests.clear();
291
589
  this.nameIndex.clear();
292
590
  this.contentCache.clear();
591
+ this.nestedSkillsCache.clear();
293
592
  const dirs = [...this.loadedDirs];
294
593
  this.loadedDirs = []; // 重置,loadFromDir 会重新填充
295
594
  for (const { dir, source, connectorId } of dirs) {
296
595
  this.loadFromDir(dir, source, connectorId);
297
596
  }
298
597
  }
598
+ /**
599
+ * 清除嵌套子包缓存
600
+ * @param packageDir 包目录路径,不传则清除所有缓存
601
+ */
602
+ clearNestedSkillsCache(packageDir) {
603
+ if (packageDir) {
604
+ this.nestedSkillsCache.delete(packageDir);
605
+ }
606
+ else {
607
+ this.nestedSkillsCache.clear();
608
+ }
609
+ }
610
+ /**
611
+ * 获取嵌套子包缓存统计信息
612
+ */
613
+ getNestedSkillsCacheStats() {
614
+ return {
615
+ size: this.nestedSkillsCache.size,
616
+ keys: Array.from(this.nestedSkillsCache.keys()),
617
+ };
618
+ }
299
619
  // ---- 内部方法 ----
300
620
  /**
301
621
  * 从磁盘懒加载 content
@@ -308,12 +628,8 @@ class SkillManager {
308
628
  if (!fs.existsSync(manifest.filePath))
309
629
  return '';
310
630
  const raw = fs.readFileSync(manifest.filePath, 'utf-8');
311
- const normalized = raw.replace(/\r\n/g, '\n');
312
- const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
313
- if (match) {
314
- return match[2].trim();
315
- }
316
- return raw;
631
+ const { data, body } = (0, frontmatter_ts_1.parseFrontmatter)(raw);
632
+ return Object.keys(data).length > 0 ? body.trim() : raw;
317
633
  }
318
634
  catch (err) {
319
635
  console.warn(`[SkillManager] Failed to lazy-load content for "${manifest.id}":`, err?.message);
@@ -349,20 +665,20 @@ class SkillManager {
349
665
  }
350
666
  /**
351
667
  * 仅解析 front matter(不读 content),用于懒加载模式
352
- * 通过读取文件前 N 字节提取 YAML front matter,避免加载整个文件
668
+ * 通过读取文件前 32KB 提取 YAML front matter,避免加载整个文件。
669
+ * 使用标准解析器,兼容嵌套 map / 多行折叠语法。
353
670
  */
354
671
  parseManifest(id, filePath, source, connectorId) {
355
672
  try {
356
- // 只读取文件前 2KB 来提取 front matter
673
+ // 读取文件前 32KB 提取 front matter(覆盖长 description / metadata 场景)
357
674
  const fd = fs.openSync(filePath, 'r');
358
- const buf = Buffer.alloc(2048);
359
- const bytesRead = fs.readSync(fd, buf, 0, 2048, 0);
675
+ const buf = Buffer.alloc(32768);
676
+ const bytesRead = fs.readSync(fd, buf, 0, 32768, 0);
360
677
  fs.closeSync(fd);
361
678
  const head = buf.toString('utf-8', 0, bytesRead);
362
- const normalized = head.replace(/\r\n/g, '\n');
363
- const match = normalized.match(/^---\n([\s\S]*?)\n---/);
364
679
  const fileSize = fs.statSync(filePath).size;
365
- if (!match) {
680
+ const { data } = (0, frontmatter_ts_1.parseFrontmatter)(head);
681
+ if (Object.keys(data).length === 0) {
366
682
  // 无 front matter,使用文件名作为 name
367
683
  return {
368
684
  id,
@@ -378,23 +694,9 @@ class SkillManager {
378
694
  ...(connectorId ? { metadata: { connectorId } } : {}),
379
695
  };
380
696
  }
381
- const fm = match[1];
382
- const referencedSkills = this.parseTags(this.extractField(fm, 'referenced_skills') || this.extractField(fm, 'referencedSkills') || '');
383
- const manifest = {
384
- id,
385
- name: this.extractField(fm, 'name') || id,
386
- description: this.extractField(fm, 'description') || '',
387
- tags: this.parseTags(this.extractField(fm, 'tags') || ''),
388
- aliases: this.parseTags(this.extractField(fm, 'aliases') || ''),
389
- source,
390
- version: this.extractField(fm, 'version') || undefined,
391
- author: this.extractField(fm, 'author') || undefined,
392
- requires: this.parseTags(this.extractField(fm, 'requires') || ''),
393
- referencedSkills: referencedSkills.length > 0 ? referencedSkills : undefined,
394
- filePath,
395
- contentSize: fileSize,
396
- ...(connectorId ? { metadata: { connectorId } } : {}),
397
- };
697
+ const manifest = this.buildManifest(id, data, source, filePath, fileSize);
698
+ if (connectorId)
699
+ manifest.metadata = { ...manifest.metadata, connectorId };
398
700
  return manifest;
399
701
  }
400
702
  catch (err) {
@@ -402,12 +704,61 @@ class SkillManager {
402
704
  return null;
403
705
  }
404
706
  }
707
+ /**
708
+ * 由解析后的 frontmatter data 构建 SkillManifest(公共逻辑,parse/parseManifest 复用)。
709
+ * content 字段由调用方按需补充(parse 全量 / parseManifest 仅清单)。
710
+ */
711
+ buildManifest(id, data, source, filePath, contentSize) {
712
+ const metadataMap = (0, frontmatter_ts_1.fmMap)(data, 'metadata');
713
+ const tags = [...(0, frontmatter_ts_1.fmStringArray)(data, 'tags', 'keywords'), ...(0, frontmatter_ts_1.fmStringArray)(metadataMap ?? {}, 'tags')];
714
+ const aliases = [...(0, frontmatter_ts_1.fmStringArray)(data, 'aliases'), ...(0, frontmatter_ts_1.fmStringArray)(metadataMap ?? {}, 'aliases')];
715
+ const requires = (0, frontmatter_ts_1.fmStringArray)(data, 'requires', 'required_mcps');
716
+ const referencedSkills = (0, frontmatter_ts_1.fmStringArray)(data, 'referenced_skills', 'referencedSkills');
717
+ const allowedTools = (0, frontmatter_ts_1.fmStringArray)(data, 'allowed-tools', 'allowedTools', 'allowed_tools');
718
+ const manifest = {
719
+ id,
720
+ name: (0, frontmatter_ts_1.fmString)(data, 'name') || id,
721
+ description: (0, frontmatter_ts_1.fmString)(data, 'description'),
722
+ tags,
723
+ aliases,
724
+ source,
725
+ version: (0, frontmatter_ts_1.fmString)(data, 'version') || (0, frontmatter_ts_1.fmString)(metadataMap ?? {}, 'version') || undefined,
726
+ author: (0, frontmatter_ts_1.fmString)(data, 'author') || (0, frontmatter_ts_1.fmString)(metadataMap ?? {}, 'author') || undefined,
727
+ requires: requires.length ? requires : [],
728
+ referencedSkills: referencedSkills.length ? referencedSkills : undefined,
729
+ filePath,
730
+ contentSize,
731
+ license: (0, frontmatter_ts_1.fmString)(data, 'license') || undefined,
732
+ compatibility: (0, frontmatter_ts_1.fmString)(data, 'compatibility') || undefined,
733
+ allowedTools: allowedTools.length ? allowedTools : undefined,
734
+ };
735
+ // 未识别的扩展字段归入 metadata(标准 parser 忽略未知 key,此处保留以便回写)
736
+ const extra = {};
737
+ for (const [k, v] of Object.entries(data)) {
738
+ if (!KNOWN_FM_KEYS.has(k))
739
+ extra[k] = v;
740
+ }
741
+ if (Object.keys(extra).length)
742
+ manifest.metadata = extra;
743
+ return manifest;
744
+ }
405
745
  /** 解析完整的 Markdown 文件(含 content),用于兼容模式 */
406
746
  parse(id, raw, source) {
407
747
  // 统一行尾为 \n(兼容 Windows CRLF)
408
748
  const normalized = raw.replace(/\r\n/g, '\n');
409
- const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
410
- if (!match) {
749
+ let { data, body } = (0, frontmatter_ts_1.parseFrontmatter)(normalized);
750
+ // 检测双重 frontmatter(旧版 createSkillFromFile 产生的问题)
751
+ const doubleMatch = body.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
752
+ if (doubleMatch) {
753
+ const fm1Fields = Object.keys(data).length;
754
+ const fm2Fields = doubleMatch[1].split('\n').filter(l => /^\w+:/.test(l)).length;
755
+ if (fm2Fields > fm1Fields) {
756
+ const reparsed = (0, frontmatter_ts_1.parseFrontmatter)(body);
757
+ data = reparsed.data;
758
+ body = reparsed.body;
759
+ }
760
+ }
761
+ if (Object.keys(data).length === 0) {
411
762
  return {
412
763
  id,
413
764
  name: id,
@@ -419,52 +770,9 @@ class SkillManager {
419
770
  requires: [],
420
771
  };
421
772
  }
422
- let frontMatter = match[1];
423
- let content = match[2].trim();
424
- // 检测双重 frontmatter(旧版 createSkillFromFile 产生的问题)
425
- const doubleMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
426
- if (doubleMatch) {
427
- const fm1Fields = frontMatter.split('\n').filter(l => /^\w+:/.test(l)).length;
428
- const fm2Fields = doubleMatch[1].split('\n').filter(l => /^\w+:/.test(l)).length;
429
- if (fm2Fields > fm1Fields) {
430
- frontMatter = doubleMatch[1];
431
- content = doubleMatch[2].trim();
432
- }
433
- }
434
- return {
435
- id,
436
- name: this.extractField(frontMatter, 'name') || id,
437
- description: this.extractField(frontMatter, 'description') || '',
438
- tags: this.parseTags(this.extractField(frontMatter, 'tags') || ''),
439
- aliases: this.parseTags(this.extractField(frontMatter, 'aliases') || ''),
440
- content,
441
- source,
442
- version: this.extractField(frontMatter, 'version') || undefined,
443
- author: this.extractField(frontMatter, 'author') || undefined,
444
- requires: this.parseTags(this.extractField(frontMatter, 'requires') || ''),
445
- referencedSkills: this.parseReferencedSkills(frontMatter),
446
- };
447
- }
448
- /** 解析 referenced_skills 字段 */
449
- parseReferencedSkills(frontMatter) {
450
- const raw = this.extractField(frontMatter, 'referenced_skills') || this.extractField(frontMatter, 'referencedSkills') || '';
451
- const tags = this.parseTags(raw);
452
- return tags.length > 0 ? tags : undefined;
453
- }
454
- extractField(frontMatter, field) {
455
- const regex = new RegExp(`^${field}:\\s*(.+)$`, 'm');
456
- const match = frontMatter.match(regex);
457
- return match ? match[1].trim() : '';
458
- }
459
- parseTags(raw) {
460
- // 支持 [tag1, tag2] 和 tag1, tag2 两种格式
461
- const cleaned = raw.replace(/^\[|\]$/g, '');
462
- if (!cleaned.trim())
463
- return [];
464
- return cleaned
465
- .split(',')
466
- .map((t) => t.trim())
467
- .filter(Boolean);
773
+ const content = body.trim();
774
+ const manifest = this.buildManifest(id, data, source, undefined, content.length);
775
+ return { ...manifest, content };
468
776
  }
469
777
  }
470
778
  exports.SkillManager = SkillManager;