package skill import ( "bytes" "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/sdaduanbilei/agent-team/internal/llm" "gopkg.in/yaml.v3" ) type Meta struct { Name string `yaml:"name"` Description string `yaml:"description"` Path string `yaml:"-"` } type Skill struct { Meta Body string // full SKILL.md body (instructions) } // Discover 递归扫描 skillsDir,返回所有包含 SKILL.md 的目录的元数据。 func Discover(skillsDir string) ([]Meta, error) { var metas []Meta filepath.Walk(skillsDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || info.Name() != "SKILL.md" { return nil } data, err := os.ReadFile(path) if err != nil { return nil } meta, err := ParseMeta(data) if err != nil { return nil } meta.Path = filepath.Dir(path) metas = append(metas, meta) return nil }) return metas, nil } // Load returns a fully loaded skill including body. func Load(skillDir string) (*Skill, error) { data, err := os.ReadFile(filepath.Join(skillDir, "SKILL.md")) if err != nil { return nil, err } meta, err := ParseMeta(data) if err != nil { return nil, err } meta.Path = skillDir body := extractBody(data) return &Skill{Meta: meta, Body: body}, nil } // ToXML generates XML for agent system prompts. // 如果 skill 有完整内容,会注入到 prompt 中。 func ToXML(metas []Meta) string { var sb strings.Builder sb.WriteString("\n") for _, m := range metas { // 尝试加载完整 skill 内容 s, err := Load(m.Path) if err == nil && s.Body != "" { fmt.Fprintf(&sb, " \n %s\n \n%s\n \n \n", m.Name, m.Description, s.Body) } else { fmt.Fprintf(&sb, " \n %s\n \n", m.Name, m.Description) } } sb.WriteString("") return sb.String() } func ParseMeta(data []byte) (Meta, error) { var meta Meta if !bytes.HasPrefix(data, []byte("---")) { return meta, fmt.Errorf("missing frontmatter") } parts := bytes.SplitN(data, []byte("---"), 3) if len(parts) < 3 { return meta, fmt.Errorf("invalid frontmatter") } return meta, yaml.Unmarshal(parts[1], &meta) } func extractBody(data []byte) string { parts := bytes.SplitN(data, []byte("---"), 3) if len(parts) < 3 { return string(data) } return strings.TrimSpace(string(parts[2])) } // ToTools 将 skill 列表转为 LLM tool 定义。 // 每个有 scripts/ 目录的 skill 生成一个 tool。 func ToTools(metas []Meta) []llm.Tool { var tools []llm.Tool for _, m := range metas { scriptsDir := filepath.Join(m.Path, "scripts") if _, err := os.Stat(scriptsDir); err != nil { continue // 没有 scripts 目录的 skill 不注册为 tool } tools = append(tools, llm.Tool{ Type: llm.ToolTypeFunction, Function: &llm.FunctionDefinition{ Name: "skill_" + strings.ReplaceAll(m.Name, "-", "_"), Description: m.Description, Parameters: json.RawMessage(`{ "type": "object", "properties": { "command": { "type": "string", "description": "要执行的完整 bash 命令,例如: bash \"$SKILLS_ROOT/` + m.Name + `/scripts/search.sh\" \"搜索关键词\" 5" } }, "required": ["command"] }`), }, }) } return tools } // SkillPathByToolName 根据 tool 函数名找到对应 skill 的路径 func SkillPathByToolName(metas []Meta, funcName string) string { for _, m := range metas { toolName := "skill_" + strings.ReplaceAll(m.Name, "-", "_") if toolName == funcName { return m.Path } } return "" }