Initial commit: CarrotAssistant backend (Go + Gin + SQLite, OpenAI-compatible agent runtime)

This commit is contained in:
2026-07-06 16:10:55 +08:00
commit 5f84739b18
40 changed files with 4781 additions and 0 deletions

100
internal/skill/manager.go Normal file
View File

@@ -0,0 +1,100 @@
package skill
import (
"fmt"
"path/filepath"
"strings"
"code.littlelan.cn/CarrotAssistant/backend/internal/store"
)
// Manager resolves skill file paths and bridges DB metadata with the on-disk
// markdown. It is stateless aside from its root directory, which comes from
// config and is shared with the HTTP layer.
type Manager struct {
// Root is the skills directory (config.SkillsDir). Each app gets a
// subdirectory named by its slug; each skill is a single .md file.
Root string
}
// NewManager constructs a Manager rooted at root.
func NewManager(root string) *Manager {
return &Manager{Root: root}
}
// FilePath returns the on-disk path for a skill, given app slug and skill slug.
func (m *Manager) FilePath(appSlug, skillSlug string) string {
return filepath.Join(m.Root, appSlug, skillSlug+".md")
}
// RelPath returns the path relative to Root (the form stored in the DB).
func (m *Manager) RelPath(appSlug, skillSlug string) string {
return filepath.ToSlash(filepath.Join(appSlug, skillSlug+".md"))
}
// Load reads a skill from disk by its DB row. The row's FilePath is expected
// to be relative to Root.
func (m *Manager) Load(row *store.Skill) (*Skill, error) {
if row == nil {
return nil, fmt.Errorf("nil skill row")
}
abs := filepath.Join(m.Root, filepath.Clean("/"+row.FilePath))
return Load(abs)
}
// Write persists a skill to disk at the path derived from app/skill slug and
// returns the relative path to store in the DB row.
func (m *Manager) Write(appSlug, skillSlug string, s *Skill) (relPath string, err error) {
s.AppSlug = appSlug
if s.Name == "" {
s.Name = skillSlug
}
rel := m.RelPath(appSlug, skillSlug)
abs := filepath.Join(m.Root, filepath.Clean("/"+rel))
if err := Write(abs, s); err != nil {
return "", err
}
return rel, nil
}
// Delete removes a skill file by its DB row's relative FilePath. Missing files
// are not an error.
func (m *Manager) Delete(relPath string) error {
if relPath == "" {
return nil
}
abs := filepath.Join(m.Root, filepath.Clean("/"+relPath))
return removeIfExists(abs)
}
// EnsureAppDir creates the per-app directory. Called when the first skill for
// an app is written.
func (m *Manager) EnsureAppDir(appSlug string) error {
return mkdirAll(filepath.Join(m.Root, appSlug))
}
// SkillSlug derives a filesystem-safe slug from a candidate, falling back to a
// random suffix when the candidate yields nothing usable.
func SkillSlug(candidate, fallback string) string {
s := slugify(candidate)
if s == "" {
s = slugify(fallback)
}
return s
}
// slugify mirrors security.Slugify but lives here to avoid an import cycle
// back into security for one helper.
func slugify(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
var b strings.Builder
for _, r := range s {
switch {
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
b.WriteRune(r)
case r == ' ' || r == '-' || r == '_':
b.WriteRune('-')
}
}
return strings.Trim(b.String(), "-")
}