package plugin // skill.go 定义插件技能类型. // // 插件技能是从插件目录的 skills/ 子目录加载的 markdown 文件, // 格式与 engine 包中的 Skill 相同(YAML frontmatter + markdown 正文). // // 与 engine.Skill 的区别: // - 增加了 PluginName 字段,标识技能来源插件 // - 名称被命名空间化:pluginName:skillName // - Source 固定为 "plugin" import ( "os" "path/filepath" "strings" ) // Skill 是一个插件技能定义. // 结构与 engine.Skill 兼容,但增加了插件来源信息. type Skill struct { // Name 技能名称(命名空间化后的名称,格式:pluginName:skillName) Name string // RawName 技能原始名称(不含插件前缀) RawName string // PluginName 来源插件名称 PluginName string // Description 技能描述 Description string // WhenToUse 何时使用此技能的说明 WhenToUse string // AllowedTools 此技能允许使用的工具列表 AllowedTools []string // Content 技能正文内容(markdown) Content string // FilePath 技能文件的原始路径 FilePath string } // FormatAsPrompt 将技能格式化为系统提示词片段. func (s *Skill) FormatAsPrompt() string { var b strings.Builder b.WriteString("## Skill: ") b.WriteString(s.Name) b.WriteString("\n") if s.Description != "" { b.WriteString("Description: ") b.WriteString(s.Description) b.WriteString("\n") } if s.WhenToUse != "" { b.WriteString("When to use: ") b.WriteString(s.WhenToUse) b.WriteString("\n") } if len(s.AllowedTools) > 0 { b.WriteString("Allowed tools: ") b.WriteString(strings.Join(s.AllowedTools, ", ")) b.WriteString("\n") } b.WriteString("\n") b.WriteString(s.Content) b.WriteString("\n") return b.String() } // LoadPluginSkills 从插件的技能目录加载所有技能文件. // // 参数: // - skillsDir: 技能目录的绝对路径 // - pluginName: 插件名称(用于命名空间化) // // 返回加载的技能列表.跳过无法解析的文件. func LoadPluginSkills(skillsDir, pluginName string) ([]*Skill, error) { entries, err := os.ReadDir(skillsDir) if err != nil { if os.IsNotExist(err) { return nil, nil // 目录不存在不是错误 } return nil, err } var skills []*Skill for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() if !strings.HasSuffix(name, ".md") { continue } filePath := filepath.Join(skillsDir, name) skill, err := loadSkillFile(filePath, pluginName) if err != nil { // 跳过解析失败的技能文件 continue } skills = append(skills, skill) } return skills, nil } // loadSkillFile 加载并解析单个技能文件. func loadSkillFile(path, pluginName string) (*Skill, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } content := string(data) skill := &Skill{ PluginName: pluginName, FilePath: path, } // 解析 YAML frontmatter frontmatter, body := parseFrontmatter(content) skill.Content = strings.TrimSpace(body) // 从 frontmatter 提取字段 if frontmatter != "" { parseSkillFrontmatterFields(frontmatter, skill) } // 如果没有原始名称,使用文件名(去掉 .md 后缀) if skill.RawName == "" { base := filepath.Base(path) skill.RawName = strings.TrimSuffix(base, ".md") } // 设置命名空间化的名称 skill.Name = pluginName + ":" + skill.RawName return skill, nil } // --- YAML frontmatter 解析(与 engine/skill_loader.go 中的实现一致)--- // parseFrontmatter 提取 YAML frontmatter 和正文. func parseFrontmatter(content string) (frontmatter string, body string) { trimmed := strings.TrimSpace(content) if !strings.HasPrefix(trimmed, "---") { return "", content } rest := trimmed[3:] if idx := strings.Index(rest, "\n"); idx >= 0 { rest = rest[idx+1:] } else { return "", content } endIdx := strings.Index(rest, "\n---") if endIdx < 0 { return "", content } frontmatter = rest[:endIdx] body = rest[endIdx+4:] if len(body) > 0 && body[0] == '\n' { body = body[1:] } return frontmatter, body } // parseSkillFrontmatterFields 从 YAML frontmatter 中提取技能字段. func parseSkillFrontmatterFields(fm string, skill *Skill) { lines := strings.Split(fm, "\n") currentKey := "" for _, line := range lines { trimmed := strings.TrimSpace(line) if trimmed == "" { continue } // 检查列表项 if strings.HasPrefix(trimmed, "- ") { value := strings.TrimSpace(trimmed[2:]) if currentKey == "allowed_tools" { skill.AllowedTools = append(skill.AllowedTools, value) } continue } // 检查 key: value 格式 colonIdx := strings.Index(trimmed, ":") if colonIdx < 0 { continue } key := strings.TrimSpace(trimmed[:colonIdx]) value := strings.TrimSpace(trimmed[colonIdx+1:]) currentKey = key switch key { case "name": skill.RawName = value case "description": skill.Description = value case "when_to_use": skill.WhenToUse = value case "allowed_tools": if value != "" && value != "|" { value = strings.Trim(value, "[]") parts := strings.Split(value, ",") for _, p := range parts { p = strings.TrimSpace(p) if p != "" { skill.AllowedTools = append(skill.AllowedTools, p) } } } } } }