package prompt import ( "fmt" "io/fs" "os" "path/filepath" "strings" ) // Skill 定义了从 SKILL.md 中解析出的标准化技能结构 type Skill struct { Name string Description string Body string } // SkillSummary 是技能的轻量目录项(仅元信息,不含 Body) type SkillSummary struct { Name string Description string } // SkillLoader 负责从本地文件系统中加载技能模板 type SkillLoader struct { workDir string } func NewSkillLoader(workDir string) *SkillLoader { return &SkillLoader{workDir: workDir} } // List 扫描 skills 目录,返回所有技能的轻量目录(仅名称和描述) func (s *SkillLoader) List() []SkillSummary { skillBaseDir := filepath.Join(s.workDir, "skills") if _, err := os.Stat(skillBaseDir); os.IsNotExist(err) { return nil } var summaries []SkillSummary filepath.WalkDir(skillBaseDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() || d.Name() != "SKILL.md" { return nil } content, err := os.ReadFile(path) if err != nil { return nil } skill := parseSkillMD(string(content)) summaries = append(summaries, SkillSummary{ Name: skill.Name, Description: skill.Description, }) return nil }) return summaries } // Load 按技能名称加载完整的 Skill(含 Body) func (s *SkillLoader) Load(name string) (*Skill, error) { skillBaseDir := filepath.Join(s.workDir, "skills") if _, err := os.Stat(skillBaseDir); os.IsNotExist(err) { return nil, fmt.Errorf("skills 目录不存在") } var found *Skill filepath.WalkDir(skillBaseDir, func(path string, d fs.DirEntry, err error) error { if err != nil || found != nil { return nil } if d.IsDir() || d.Name() != "SKILL.md" { return nil } content, err := os.ReadFile(path) if err != nil { return nil } skill := parseSkillMD(string(content)) if skill.Name == name { found = &skill } return nil }) if found == nil { return nil, fmt.Errorf("未找到技能: %s", name) } return found, nil } // parseSkillMD 解析带有 YAML Frontmatter 的 Markdown 内容 func parseSkillMD(content string) Skill { skill := Skill{ Name: "Unknown Skill", Description: "No description provided.", Body: content, } if strings.HasPrefix(content, "---\n") || strings.HasPrefix(content, "---\r\n") { parts := strings.SplitN(content, "---", 3) if len(parts) == 3 { frontmatter := parts[1] skill.Body = strings.TrimSpace(parts[2]) lines := strings.Split(frontmatter, "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "name:") { skill.Name = strings.TrimSpace(strings.TrimPrefix(line, "name:")) } else if strings.HasPrefix(line, "description:") { skill.Description = strings.TrimSpace(strings.TrimPrefix(line, "description:")) } } } } return skill }