free-code-dotnet/docs/UI与扩展设计/UI与扩展设计-技能系统.md
应文浩wenhao.ying@xiaobao100.com e25ac591a7 init easy-code
2026-04-06 07:24:24 +08:00

266 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 技能系统
> 所属项目: 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
/// <summary>
/// 技能加载器 — 从 .free-code/skills/ 目录加载 SKILL.md
/// 对应原始 skill system 的目录扫描 + frontmatter 解析
/// </summary>
public interface ISkillLoader
{
Task<IReadOnlyList<SkillDefinition>> LoadAllSkillsAsync();
Task<SkillDefinition?> LoadSkillAsync(string skillName);
Task ExecuteSkillAsync(SkillDefinition skill, string? args);
}
```
### SkillDefinition
```csharp
/// <summary>
/// 技能定义 — 对应一个 SKILL.md 文件的完整解析结果
/// </summary>
public sealed record SkillDefinition
{
/// <summary>技能名称,来自 frontmatter 或文件名</summary>
public required string Name { get; init; }
/// <summary>技能描述,用于斜杠命令帮助文本</summary>
public string? Description { get; init; }
/// <summary>Markdown 正文,作为 System Prompt 注入</summary>
public required string Content { get; init; }
/// <summary>技能允许使用的工具名称列表</summary>
public IReadOnlyList<string> Tools { get; init; } = [];
/// <summary>覆盖默认模型(如 "claude-haiku-4-5"</summary>
public string? Model { get; init; }
/// <summary>技能参数定义(参数名 → 描述)</summary>
public IReadOnlyDictionary<string, string>? Arguments { get; init; }
/// <summary>生命周期钩子</summary>
public SkillHooks? Hooks { get; init; }
/// <summary>磁盘路径,仅供调试</summary>
public string? FilePath { get; init; }
}
```
### SkillHooks
```csharp
/// <summary>
/// 技能生命周期钩子
/// 对应原始 skill hooks 机制
/// </summary>
public sealed record SkillHooks
{
/// <summary>执行前的提示注入片段</summary>
public string? PreExecute { get; init; }
/// <summary>执行后的提示注入片段</summary>
public string? PostExecute { get; init; }
/// <summary>发生错误时的提示注入片段</summary>
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<SkillDefinition>? _cached;
private readonly ILogger<SkillLoader> _logger;
public SkillLoader(ILogger<SkillLoader> 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<IReadOnlyList<SkillDefinition>> 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<SkillDefinition?> LoadSkillAsync(string skillName)
{
var all = await LoadAllSkillsAsync();
return all.FirstOrDefault(s =>
string.Equals(s.Name, skillName, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// 解析 SKILL.md — YAML frontmatter + Markdown body
/// </summary>
private static async Task<SkillDefinition?> 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<Dictionary<string, object>>(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<object>)
?.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)