// Package skill 提供 Skill 定义和管理系统。 package skill import ( "fmt" "os" "path/filepath" "sort" "strings" ) // frontmatterDelim 是 SKILL.md 文件中 YAML 前置元数据的分隔符。 const ( frontmatterDelim = "---" ) // ParseSkillFile 解析 SKILL.md 文件并返回填充好的 Skill 结构体。 // // 预期格式为: // // --- // name: my-skill // description: Does something useful // triggers: ["keyword1", "keyword2"] // --- // // # My Skill // // Detailed description... func ParseSkillFile(path string) (*Skill, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("skill: cannot read %q: %w", path, err) } return ParseSkillData(path, data) } // ParseSkillData 从原始字节解析 SKILL.md 内容。 // path 参数用于定位 scripts/ 目录。 func ParseSkillData(path string, data []byte) (*Skill, error) { content := string(data) skill := &Skill{ Path: path, Body: content, Triggers: []string{}, } // Parse YAML frontmatter rest, err := parseFrontmatter(content, skill) if err != nil { return nil, err } skill.Body = strings.TrimSpace(rest) // Validate required fields if skill.Name == "" { return nil, fmt.Errorf("skill: %q is missing 'name' in frontmatter", path) } // Discover scripts directory skillDir := filepath.Dir(path) scriptsDir := filepath.Join(skillDir, "scripts") skill.ScriptsDir = scriptsDir if info, err := os.Stat(scriptsDir); err == nil && info.IsDir() { scripts, err := discoverScripts(scriptsDir) if err != nil { return nil, fmt.Errorf("skill: failed to discover scripts in %q: %w", scriptsDir, err) } skill.Scripts = scripts } return skill, nil } // parseFrontmatter 提取由 "---" 行分隔的 YAML 前置元数据 // 并填充 Skill 结构体字段。 func parseFrontmatter(content string, skill *Skill) (string, error) { content = strings.TrimSpace(content) if !strings.HasPrefix(content, frontmatterDelim) { // No frontmatter — treat entire content as body return content, nil } // Find the closing delimiter rest := content[len(frontmatterDelim):] rest = strings.TrimLeft(rest, "\n\r") endIdx := strings.Index(rest, "\n"+frontmatterDelim) if endIdx < 0 { // Also check for end-of-file style endIdx = strings.Index(rest, frontmatterDelim) if endIdx < 0 { return "", fmt.Errorf("skill: unclosed frontmatter in skill file") } } frontmatter := rest[:endIdx] body := rest[endIdx+len(frontmatterDelim)+1:] // Parse the YAML frontmatter (simple key-value parser) if err := parseSimpleYAML(frontmatter, skill); err != nil { return "", err } return body, nil } // parseSimpleYAML 解析技能前置元数据的简化 YAML 格式。 // 支持:字符串值、带引号的字符串和数组值。 func parseSimpleYAML(yaml string, skill *Skill) error { lines := strings.Split(yaml, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } colonIdx := strings.Index(line, ":") if colonIdx < 0 { continue } key := strings.TrimSpace(line[:colonIdx]) value := strings.TrimSpace(line[colonIdx+1:]) switch key { case "name": skill.Name = trimQuotes(value) case "description": skill.Description = trimQuotes(value) case "triggers": triggers, err := parseYAMLArray(value) if err != nil { return fmt.Errorf("skill: invalid triggers format: %w", err) } skill.Triggers = triggers } } return nil } // parseYAMLArray 解析 YAML 数组,如 '["a", "b", "c"]' 或 '[a, b, c]'。 func parseYAMLArray(value string) ([]string, error) { value = strings.TrimSpace(value) // Handle YAML list format: ["a", "b"] or [a, b] if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") { inner := value[1 : len(value)-1] if strings.TrimSpace(inner) == "" { return []string{}, nil } parts := splitCommas(inner) result := make([]string, len(parts)) for i, p := range parts { result[i] = trimQuotes(strings.TrimSpace(p)) } return result, nil } // Handle YAML list format with dashes: // triggers: // - browser // - navigate // (This would be handled line-by-line in a different flow) // For now, treat single value as a one-element list if value != "" && value != "[]" { return []string{trimQuotes(value)}, nil } return []string{}, nil } // splitCommas 分割逗号分隔的字符串,尊重引号部分。 func splitCommas(s string) []string { var parts []string var current strings.Builder inQuote := false quoteChar := byte(0) for i := 0; i < len(s); i++ { c := s[i] if inQuote { current.WriteByte(c) if c == quoteChar { inQuote = false } } else if c == '"' || c == '\'' { current.WriteByte(c) inQuote = true quoteChar = c } else if c == ',' { parts = append(parts, current.String()) current.Reset() } else { current.WriteByte(c) } } if current.Len() > 0 { parts = append(parts, current.String()) } return parts } // trimQuotes 移除字符串值周围的引号。 func trimQuotes(s string) string { s = strings.TrimSpace(s) if len(s) >= 2 { if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') { return s[1 : len(s)-1] } } return s } // discoverScripts 列出 scripts 目录中所有可执行/可读的文件。 func discoverScripts(scriptsDir string) ([]string, error) { entries, err := os.ReadDir(scriptsDir) if err != nil { return nil, err } var scripts []string for _, entry := range entries { if entry.IsDir() { continue } scripts = append(scripts, entry.Name()) } sort.Strings(scripts) return scripts, nil } // LoadSkillFromDir 从包含 SKILL.md 文件的目录加载技能。 func LoadSkillFromDir(dir string) (*Skill, error) { skillPath := filepath.Join(dir, "SKILL.md") if _, err := os.Stat(skillPath); os.IsNotExist(err) { return nil, fmt.Errorf("skill: no SKILL.md found in %q", dir) } return ParseSkillFile(skillPath) }