2026-03-11 16:25:48 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"x-agents/server/internal/model"
|
|
|
|
|
|
"x-agents/server/internal/repository"
|
|
|
|
|
|
|
|
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type SkillService struct {
|
|
|
|
|
|
skillRepo *repository.SkillRepository
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewSkillService(skillRepo *repository.SkillRepository) *SkillService {
|
|
|
|
|
|
return &SkillService{skillRepo: skillRepo}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SkillService) GetAllSkills() ([]model.Skill, error) {
|
|
|
|
|
|
return s.skillRepo.FindAll()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SkillService) GetSkillByID(id string) (*model.Skill, error) {
|
|
|
|
|
|
return s.skillRepo.FindByID(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SkillService) GetSkillsByType(skillType string) ([]model.Skill, error) {
|
|
|
|
|
|
return s.skillRepo.FindByType(skillType)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SkillService) CreateSkill(skill *model.Skill) error {
|
|
|
|
|
|
return s.skillRepo.Create(skill)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SkillService) UpdateSkill(skill *model.Skill) error {
|
|
|
|
|
|
return s.skillRepo.Update(skill)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *SkillService) DeleteSkill(id string) error {
|
2026-03-12 10:49:44 +08:00
|
|
|
|
// 先获取 skill 信息,以便删除本地文件
|
|
|
|
|
|
skill, err := s.skillRepo.FindByID(id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除数据库记录
|
|
|
|
|
|
if err := s.skillRepo.Delete(id); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除本地文件(skill 目录)
|
|
|
|
|
|
if skill.Path != "" {
|
|
|
|
|
|
// 获取 skill 所在目录(SKILL.md 的父目录)
|
|
|
|
|
|
skillDir := filepath.Dir(skill.Path)
|
|
|
|
|
|
if err := os.RemoveAll(skillDir); err != nil {
|
|
|
|
|
|
log.Printf("[SkillService] Warning: failed to delete skill directory %s: %v", skillDir, err)
|
|
|
|
|
|
// 数据库记录已删除,不返回错误
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log.Printf("[SkillService] Deleted skill directory: %s", skillDir)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
2026-03-11 16:25:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// InitSkills 初始化扫描所有 skills 目录
|
|
|
|
|
|
func (s *SkillService) InitSkills() error {
|
|
|
|
|
|
// 获取项目根目录
|
|
|
|
|
|
projectRoot := s.getProjectRoot()
|
|
|
|
|
|
if projectRoot == "" {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:49:44 +08:00
|
|
|
|
var totalCount int
|
|
|
|
|
|
|
2026-03-12 15:23:22 +08:00
|
|
|
|
// 扫描 system skills: core/agents/skills/system
|
|
|
|
|
|
systemSkillsPath := filepath.Join(projectRoot, "core", "agents", "skills", "system")
|
2026-03-11 16:25:48 +08:00
|
|
|
|
if _, err := os.Stat(systemSkillsPath); err == nil {
|
|
|
|
|
|
systemSkills, err := s.scanSkillsDirectory(systemSkillsPath, "system")
|
2026-03-12 10:49:44 +08:00
|
|
|
|
if err == nil && len(systemSkills) > 0 {
|
2026-03-11 16:25:48 +08:00
|
|
|
|
s.skillRepo.DeleteByType("system")
|
2026-03-12 10:49:44 +08:00
|
|
|
|
s.skillRepo.UpsertBatch(systemSkills)
|
|
|
|
|
|
totalCount += len(systemSkills)
|
2026-03-11 16:25:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:23:22 +08:00
|
|
|
|
// 扫描 user skills: core/agents/skills/user
|
|
|
|
|
|
userSkillsPath := filepath.Join(projectRoot, "core", "agents", "skills", "user")
|
|
|
|
|
|
if _, err := os.Stat(userSkillsPath); err == nil {
|
|
|
|
|
|
userSkills, err := s.scanSkillsDirectory(userSkillsPath, "user")
|
|
|
|
|
|
if err == nil && len(userSkills) > 0 {
|
|
|
|
|
|
s.skillRepo.DeleteByType("user")
|
|
|
|
|
|
s.skillRepo.UpsertBatch(userSkills)
|
|
|
|
|
|
totalCount += len(userSkills)
|
2026-03-11 16:25:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:49:44 +08:00
|
|
|
|
if totalCount > 0 {
|
|
|
|
|
|
log.Printf("[SkillService] Loaded %d skills", totalCount)
|
|
|
|
|
|
}
|
2026-03-11 16:25:48 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// scanSkillsDirectory 扫描指定目录下的所有 skill
|
|
|
|
|
|
func (s *SkillService) scanSkillsDirectory(basePath string, skillType string) ([]model.Skill, error) {
|
|
|
|
|
|
var skills []model.Skill
|
|
|
|
|
|
|
|
|
|
|
|
entries, err := os.ReadDir(basePath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, entry := range entries {
|
|
|
|
|
|
if !entry.IsDir() {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
skillDir := filepath.Join(basePath, entry.Name())
|
|
|
|
|
|
skillPath := filepath.Join(skillDir, "SKILL.md")
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试 skill.md(大小写不敏感)
|
|
|
|
|
|
if _, err := os.Stat(skillPath); os.IsNotExist(err) {
|
|
|
|
|
|
skillPath = filepath.Join(skillDir, "skill.md")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
skillInfo, err := s.parseSkillFile(skillPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("[SkillService] Error parsing skill file %s: %v", skillPath, err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有从文件解析到名称,使用目录名
|
|
|
|
|
|
if skillInfo.SkillName == "" {
|
|
|
|
|
|
skillInfo.SkillName = entry.Name()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
skill := model.Skill{
|
|
|
|
|
|
SkillName: skillInfo.SkillName,
|
|
|
|
|
|
SkillType: skillType,
|
|
|
|
|
|
SkillDesc: skillInfo.SkillDesc,
|
|
|
|
|
|
Path: skillPath,
|
2026-03-13 08:31:57 +08:00
|
|
|
|
Status: "active",
|
2026-03-11 16:25:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
skills = append(skills, skill)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return skills, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// parseSkillFile 解析 SKILL.md 文件,提取 YAML front matter
|
|
|
|
|
|
func (s *SkillService) parseSkillFile(skillPath string) (*model.Skill, error) {
|
|
|
|
|
|
content, err := os.ReadFile(skillPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有 YAML front matter
|
|
|
|
|
|
contentStr := string(content)
|
|
|
|
|
|
if !strings.HasPrefix(contentStr, "---") {
|
|
|
|
|
|
// 没有 front matter,返回空
|
|
|
|
|
|
return &model.Skill{}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 找到结束标记
|
|
|
|
|
|
lines := strings.Split(contentStr, "\n")
|
|
|
|
|
|
var endIdx int
|
|
|
|
|
|
for i := 1; i < len(lines); i++ {
|
|
|
|
|
|
if strings.TrimSpace(lines[i]) == "---" {
|
|
|
|
|
|
endIdx = i
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if endIdx == 0 {
|
|
|
|
|
|
return &model.Skill{}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提取 YAML 内容
|
|
|
|
|
|
yamlContent := strings.Join(lines[1:endIdx], "\n")
|
|
|
|
|
|
|
|
|
|
|
|
// 解析 YAML
|
|
|
|
|
|
var frontMatter map[string]string
|
|
|
|
|
|
if err := yaml.Unmarshal([]byte(yamlContent), &frontMatter); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
skill := &model.Skill{
|
|
|
|
|
|
SkillName: frontMatter["name"],
|
|
|
|
|
|
SkillDesc: frontMatter["description"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return skill, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getProjectRoot 获取项目根目录
|
|
|
|
|
|
func (s *SkillService) getProjectRoot() string {
|
|
|
|
|
|
execPath, _ := os.Getwd()
|
|
|
|
|
|
projectRoot := execPath
|
|
|
|
|
|
|
|
|
|
|
|
// 如果当前目录名为 server,向上找一级
|
|
|
|
|
|
baseName := filepath.Base(execPath)
|
|
|
|
|
|
if baseName == "server" {
|
|
|
|
|
|
projectRoot = filepath.Dir(execPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试向上查找包含 .git 的目录
|
|
|
|
|
|
if _, err := os.Stat(filepath.Join(projectRoot, ".git")); os.IsNotExist(err) {
|
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
|
parent := filepath.Dir(projectRoot)
|
|
|
|
|
|
if parent == projectRoot {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := os.Stat(filepath.Join(parent, ".git")); err == nil {
|
|
|
|
|
|
projectRoot = parent
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
projectRoot = parent
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return projectRoot
|
|
|
|
|
|
}
|