# 技能系统 > 所属项目: free-code .NET 10 重写 > 所属模块: [UI 与扩展设计](UI与扩展设计.md) > 原始代码: `../../src/skills/` --- ## 概述 技能系统允许用户以 Markdown 文件的形式定义可复用的指令集,通过斜杠命令或提示注入的方式触发。原始实现扫描 `.free-code/skills/` 目录,解析每个文件的 YAML frontmatter 获取元数据,并将 Markdown 正文作为 System Prompt 内容注入查询引擎。 .NET 重写通过 `ISkillLoader` 接口封装这一流程,用 `SkillDefinition` record 描述单条技能,用 `SkillHooks` record 描述生命周期钩子。`SkillLoader` 是默认实现,负责目录扫描、YAML 解析、结果缓存和执行委托。 --- ## 16.1 接口与数据模型 ```csharp /// /// 技能加载器 — 从 .free-code/skills/ 目录加载 SKILL.md /// 对应原始 skill system 的目录扫描 + frontmatter 解析 /// public interface ISkillLoader { Task> LoadAllSkillsAsync(); Task LoadSkillAsync(string skillName); Task ExecuteSkillAsync(SkillDefinition skill, string? args); } ``` ### SkillDefinition ```csharp /// /// 技能定义 — 对应一个 SKILL.md 文件的完整解析结果 /// public sealed record SkillDefinition { /// 技能名称,来自 frontmatter 或文件名 public required string Name { get; init; } /// 技能描述,用于斜杠命令帮助文本 public string? Description { get; init; } /// Markdown 正文,作为 System Prompt 注入 public required string Content { get; init; } /// 技能允许使用的工具名称列表 public IReadOnlyList Tools { get; init; } = []; /// 覆盖默认模型(如 "claude-haiku-4-5") public string? Model { get; init; } /// 技能参数定义(参数名 → 描述) public IReadOnlyDictionary? Arguments { get; init; } /// 生命周期钩子 public SkillHooks? Hooks { get; init; } /// 磁盘路径,仅供调试 public string? FilePath { get; init; } } ``` ### SkillHooks ```csharp /// /// 技能生命周期钩子 /// 对应原始 skill hooks 机制 /// public sealed record SkillHooks { /// 执行前的提示注入片段 public string? PreExecute { get; init; } /// 执行后的提示注入片段 public string? PostExecute { get; init; } /// 发生错误时的提示注入片段 public string? OnError { get; init; } } ``` **设计意图** `SkillDefinition` 使用 `sealed record` 确保不可变性:一旦从磁盘解析完成,技能定义在整个会话生命周期内不会改变。`Tools` 字段对应原始 frontmatter 中的 `tools` 数组,允许技能声明自己需要哪些工具,`ToolRegistry` 在组装工具池时可据此过滤。 --- ## 16.2 SkillLoader 实现 ```csharp public sealed class SkillLoader : ISkillLoader { private readonly string[] _skillDirectories; private List? _cached; private readonly ILogger _logger; public SkillLoader(ILogger logger) { _logger = logger; var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var cwd = Directory.GetCurrentDirectory(); // 扫描顺序: 项目级 → 用户级 _skillDirectories = [ Path.Combine(cwd, ".free-code", "skills"), Path.Combine(home, ".free-code", "skills"), ]; } public async Task> LoadAllSkillsAsync() { if (_cached != null) return _cached; _cached = []; foreach (var dir in _skillDirectories) { if (!Directory.Exists(dir)) continue; foreach (var file in Directory.GetFiles(dir, "*.md", SearchOption.AllDirectories)) { try { var skill = await ParseSkillFileAsync(file); if (skill != null) _cached.Add(skill); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to load skill from {File}", file); } } } return _cached; } public async Task LoadSkillAsync(string skillName) { var all = await LoadAllSkillsAsync(); return all.FirstOrDefault(s => string.Equals(s.Name, skillName, StringComparison.OrdinalIgnoreCase)); } /// /// 解析 SKILL.md — YAML frontmatter + Markdown body /// private static async Task ParseSkillFileAsync(string filePath) { var content = await File.ReadAllTextAsync(filePath); if (!content.StartsWith("---")) return null; var endIdx = content.IndexOf("\n---", 3); if (endIdx < 0) return null; var frontmatter = content[3..endIdx]; var body = content[(endIdx + 4)..].TrimStart(); // 解析 YAML frontmatter var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); var meta = deserializer.Deserialize>(frontmatter); return new SkillDefinition { Name = meta.GetValueOrDefault("name", Path.GetFileNameWithoutExtension(filePath))?.ToString() ?? "", Description = meta.GetValueOrDefault("description")?.ToString(), Content = body, Tools = (meta.GetValueOrDefault("tools") as List) ?.Select(o => o.ToString() ?? "") .ToList() ?? [], Model = meta.GetValueOrDefault("model")?.ToString(), FilePath = filePath, }; } public async Task ExecuteSkillAsync(SkillDefinition skill, string? args) { // 技能内容注入 System Prompt,参数拼接到末尾,由 QueryEngine 执行 var fullPrompt = skill.Content; if (args != null) fullPrompt += $"\n\nArguments: {args}"; // 通过 QueryEngine 提交,渲染到 UI var engine = /* 从 IServiceProvider 解析 */; await foreach (var msg in engine.SubmitMessageAsync(fullPrompt)) { // UI 渲染逻辑 } } } ``` --- ## 目录扫描与优先级 `SkillLoader` 按以下顺序扫描两个目录,先找到的技能名称优先: | 优先级 | 路径 | 说明 | |---|---|---| | 1(高)| `<当前工作目录>/.free-code/skills/` | 项目级技能,随代码库版本管理 | | 2(低)| `~/.free-code/skills/` | 用户级技能,跨项目共享 | 同名技能以项目级为准,允许项目级覆盖用户级的默认行为。 --- ## SKILL.md 文件格式 ```markdown --- name: code-review description: 对当前分支的变更进行代码审查 tools: - Bash - Read model: claude-sonnet-4-6 --- 你是一位严谨的代码审查员。分析提供的代码变更,重点关注: - 潜在的 bug 和边界条件 - 安全漏洞 - 性能问题 - 代码风格与项目规范的一致性 输出格式:按严重性分组,每条问题标注文件和行号。 ``` frontmatter 字段说明: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `name` | `string` | 否 | 技能名称,默认使用文件名 | | `description` | `string` | 否 | 显示在斜杠命令帮助中的描述 | | `tools` | `string[]` | 否 | 允许使用的工具,空则使用全部 | | `model` | `string` | 否 | 覆盖默认模型 | | `arguments` | `object` | 否 | 参数定义(名称 → 描述) | --- ## 生命周期钩子执行顺序 ``` ExecuteSkillAsync(skill, args) │ ├── 1. hooks.PreExecute → 注入到提示前缀 ├── 2. skill.Content → 主提示内容 ├── 3. args → 用户传入参数 │ └── QueryEngine.SubmitMessageAsync(fullPrompt) │ └── [成功] hooks.PostExecute └── [失败] hooks.OnError ``` --- ## 参考资料 - [UI 与扩展设计 — 总览](UI与扩展设计.md) - [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md) - [核心模块设计 — 命令系统](../核心模块设计/核心模块设计-命令系统.md) - [UI 与扩展设计 — 插件系统](UI与扩展设计-插件系统.md)