Files
HomeAgent/internal/skill/manager.go
root bc26850b50 feat: complete HomeAgent architecture v2
- IO abstraction layer with OutputChannel routing and capability validation
- Three-layer memory (Context-Document-Graph) with TF-IDF relevance pruning
- OneBot V11 QQ protocol plugin with Reverse WebSocket client
- Plugin system with hot-reload (SKILL.md + native factories)
- Knowledge system with TF-IDF vector indexing
- Personality system (personal.md)
- Text memory (JSONL with rotation)
- Change tracker (overlayfs) with rollback
- Lua adapter VM
- Design document (DESIGN.md)

Module: gitcode.com/JianFeeeee/HomeAgent
2026-07-02 12:04:36 +08:00

219 lines
4.7 KiB
Go

package skill
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
)
type Skill struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Author string `json:"author,omitempty"`
Entry string `json:"entry,omitempty"`
Source string `json:"source,omitempty"`
Enabled bool `json:"enabled"`
RawContent string `json:"-"`
}
type Manager struct {
mu sync.RWMutex
skillsDir string
skills map[string]*Skill
}
func NewManager(skillsDir string) *Manager {
return &Manager{
skillsDir: skillsDir,
skills: make(map[string]*Skill),
}
}
func (m *Manager) Init() error {
if err := os.MkdirAll(m.skillsDir, 0755); err != nil {
return fmt.Errorf("create skills dir: %w", err)
}
return m.loadAll()
}
func (m *Manager) loadAll() error {
entries, err := os.ReadDir(m.skillsDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
skillDir := filepath.Join(m.skillsDir, entry.Name())
skill, err := m.loadSkill(skillDir)
if err != nil {
continue
}
m.skills[skill.Name] = skill
}
return nil
}
func (m *Manager) loadSkill(dir string) (*Skill, error) {
skill := &Skill{
Name: filepath.Base(dir),
Enabled: true,
}
skillFilePath := filepath.Join(dir, "SKILL.md")
if data, err := os.ReadFile(skillFilePath); err == nil {
skill.RawContent = string(data)
skill.Description = extractDescription(skill.RawContent)
skill.Version = extractField(skill.RawContent, "version")
skill.Author = extractField(skill.RawContent, "author")
}
metaPath := filepath.Join(dir, "skill.json")
if data, err := os.ReadFile(metaPath); err == nil {
var meta struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Author string `json:"author"`
Entry string `json:"entry"`
}
if err := json.Unmarshal(data, &meta); err == nil {
if meta.Name != "" {
skill.Name = meta.Name
}
if meta.Description != "" {
skill.Description = meta.Description
}
if meta.Version != "" {
skill.Version = meta.Version
}
if meta.Author != "" {
skill.Author = meta.Author
}
if meta.Entry != "" {
skill.Entry = meta.Entry
}
}
}
return skill, nil
}
func (m *Manager) Install(name string, content string) error {
m.mu.Lock()
defer m.mu.Unlock()
skillDir := filepath.Join(m.skillsDir, name)
if err := os.MkdirAll(skillDir, 0755); err != nil {
return fmt.Errorf("create skill dir: %w", err)
}
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(content), 0644); err != nil {
return fmt.Errorf("write SKILL.md: %w", err)
}
skill, err := m.loadSkill(skillDir)
if err != nil {
return fmt.Errorf("load installed skill: %w", err)
}
m.skills[name] = skill
return nil
}
func (m *Manager) Uninstall(name string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.skills[name]; !ok {
return fmt.Errorf("skill %s not found", name)
}
skillDir := filepath.Join(m.skillsDir, name)
if err := os.RemoveAll(skillDir); err != nil {
return fmt.Errorf("remove skill dir: %w", err)
}
delete(m.skills, name)
return nil
}
func (m *Manager) List() []*Skill {
m.mu.RLock()
defer m.mu.RUnlock()
skills := make([]*Skill, 0, len(m.skills))
for _, s := range m.skills {
skills = append(skills, s)
}
sort.Slice(skills, func(i, j int) bool {
return skills[i].Name < skills[j].Name
})
return skills
}
func (m *Manager) Get(name string) *Skill {
m.mu.RLock()
defer m.mu.RUnlock()
return m.skills[name]
}
func (m *Manager) Toggle(name string, enabled bool) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.skills[name]; !ok {
return fmt.Errorf("skill %s not found", name)
}
m.skills[name].Enabled = enabled
return nil
}
func (m *Manager) GetInjectedPrompt() string {
m.mu.RLock()
defer m.mu.RUnlock()
var parts []string
for _, s := range m.skills {
if s.Enabled && s.RawContent != "" {
parts = append(parts, fmt.Sprintf("=== Skill: %s ===\n%s", s.Name, s.RawContent))
}
}
return strings.Join(parts, "\n\n")
}
func extractDescription(content string) string {
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
return line
}
}
return ""
}
func extractField(content string, field string) string {
prefix := fmt.Sprintf("%s:", field)
for _, line := range strings.Split(content, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(strings.ToLower(trimmed), prefix) {
return strings.TrimSpace(strings.TrimPrefix(trimmed, prefix))
}
}
return ""
}