// Package skill 提供 Skill 定义和管理系统。 // // 技能是从 ~/.agents/skills/ 加载的可组合能力。 // 每个技能都有一个带 YAML 前置元数据的 SKILL.md 清单文件, // 以及可选的 scripts/ 子目录中的脚本。 // 技能可以通过触发关键词被发现和调用。 package skill import ( "fmt" "os" "path/filepath" "sort" "strings" "sync" ) const ( // DefaultSkillsDir 是用户安装技能的默认目录。 DefaultSkillsDir = "~/.agents/skills" // SkillManifestFile 是技能清单文件的名称。 SkillManifestFile = "SKILL.md" ) // Manager 是一个用于加载、存储和查询技能的线程安全注册表。 // // 技能从目录树加载,每个包含 SKILL.md 文件的子目录都被视为一个技能。 // 管理器在初始化时自动发现技能,并提供按触发关键词或名称查找技能的方法。 type Manager struct { mu sync.RWMutex skillsDir string skills map[string]*Skill } // NewManager 创建一个新的技能管理器,扫描给定目录中的技能。 // 如果 skillsDir 为空,则使用 DefaultSkillsDir。 func NewManager(skillsDir string) *Manager { if skillsDir == "" { skillsDir = DefaultSkillsDir } // Expand ~ to home directory skillsDir = expandHome(skillsDir) return &Manager{ skillsDir: skillsDir, skills: make(map[string]*Skill), } } // LoadAll 扫描技能目录并加载所有找到的技能。 // 返回加载的技能数量和遇到的任何错误。 func (m *Manager) LoadAll() (int, error) { m.mu.Lock() defer m.mu.Unlock() // Clear existing skills m.skills = make(map[string]*Skill) // Check if skills directory exists info, err := os.Stat(m.skillsDir) if err != nil { if os.IsNotExist(err) { return 0, nil // No skills directory yet — not an error } return 0, fmt.Errorf("skill: cannot access skills directory %q: %w", m.skillsDir, err) } if !info.IsDir() { return 0, fmt.Errorf("skill: %q is not a directory", m.skillsDir) } // Read all entries in the skills directory entries, err := os.ReadDir(m.skillsDir) if err != nil { return 0, fmt.Errorf("skill: failed to read skills directory %q: %w", m.skillsDir, err) } var loadErrors []string loaded := 0 for _, entry := range entries { if !entry.IsDir() { continue } skillDir := filepath.Join(m.skillsDir, entry.Name()) skillPath := filepath.Join(skillDir, SkillManifestFile) if _, err := os.Stat(skillPath); os.IsNotExist(err) { continue // No SKILL.md in this directory — skip } skill, err := ParseSkillFile(skillPath) if err != nil { loadErrors = append(loadErrors, fmt.Sprintf("%s: %v", entry.Name(), err)) continue } m.skills[skill.Name] = skill loaded++ } if len(loadErrors) > 0 { return loaded, fmt.Errorf("skill: loaded %d skills with %d errors: %s", loaded, len(loadErrors), joinStrings(loadErrors, "; ")) } return loaded, nil } // GetSkill 按名称检索技能。如果未找到则返回 false。 func (m *Manager) GetSkill(name string) (*Skill, bool) { m.mu.RLock() defer m.mu.RUnlock() skill, ok := m.skills[name] return skill, ok } // ListSkills 返回按名称排序的所有已加载技能。 func (m *Manager) ListSkills() []*Skill { m.mu.RLock() defer m.mu.RUnlock() result := make([]*Skill, 0, len(m.skills)) for _, skill := range m.skills { result = append(result, skill) } sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) return result } // FindSkill 查找触发器与给定查询字符串匹配的技能。 // 返回所有匹配的技能,按相关性排序(触发器匹配多的优先)。 func (m *Manager) FindSkill(query string) []*Skill { m.mu.RLock() defer m.mu.RUnlock() var matches []*Skill for _, skill := range m.skills { if skill.MatchTrigger(query) { matches = append(matches, skill) } } // Sort by number of matching triggers (descending) sort.Slice(matches, func(i, j int) bool { return countMatches(matches[i], query) > countMatches(matches[j], query) }) return matches } // SkillsDir 返回正在扫描技能的目录。 func (m *Manager) SkillsDir() string { return m.skillsDir } // Reload 从磁盘刷新所有技能。 func (m *Manager) Reload() (int, error) { return m.LoadAll() } // countMatches 计算技能的触发器中有多少个与查询匹配。 func countMatches(skill *Skill, query string) int { count := 0 queryLower := strings.ToLower(query) for _, trigger := range skill.Triggers { if strings.Contains(queryLower, strings.ToLower(trigger)) { count++ } } return count } // expandHome replaces "~" with the user's home directory. func expandHome(path string) string { if len(path) > 0 && path[0] == '~' { home, err := os.UserHomeDir() if err != nil { return path } return filepath.Join(home, path[1:]) } return path } // joinStrings joins a slice of strings with a separator. func joinStrings(parts []string, sep string) string { if len(parts) == 0 { return "" } result := parts[0] for _, p := range parts[1:] { result += sep + p } return result }