mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
- 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
219 lines
4.7 KiB
Go
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 ""
|
|
}
|