Files
X-Agents/server/internal/service/skill_service.go
DESKTOP-72TV0V4\caoxiaozhu fc1204a033 feat: 新增 Agent、Memory、Skill 模块
- handler: agent_handler, memory_handler, skill_handler
- model: agent.go, skill.go
- repository: agent_repo, skill_repo
- service: agent_service, memory_service, skill_service
- 新增 migrations 目录

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:25:48 +08:00

227 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
return s.skillRepo.Delete(id)
}
// InitSkills 初始化扫描所有 skills 目录
func (s *SkillService) InitSkills() error {
log.Println("[SkillService] Starting init skills...")
// 获取项目根目录
projectRoot := s.getProjectRoot()
if projectRoot == "" {
log.Println("[SkillService] Cannot determine project root, skipping skill init")
return nil
}
// 扫描 system skills: account/admin/skills
systemSkillsPath := filepath.Join(projectRoot, "account", "admin", "skills")
if _, err := os.Stat(systemSkillsPath); err == nil {
log.Printf("[SkillService] Scanning system skills from: %s", systemSkillsPath)
systemSkills, err := s.scanSkillsDirectory(systemSkillsPath, "system")
if err != nil {
log.Printf("[SkillService] Error scanning system skills: %v", err)
} else {
log.Printf("[SkillService] Found %d system skills", len(systemSkills))
// 先删除旧的 system skills
s.skillRepo.DeleteByType("system")
// 批量插入
if err := s.skillRepo.UpsertBatch(systemSkills); err != nil {
log.Printf("[SkillService] Error saving system skills: %v", err)
}
}
}
// 扫描 user skills: account/{username}/skills (除了 admin)
accountPath := filepath.Join(projectRoot, "account")
entries, err := os.ReadDir(accountPath)
if err != nil {
log.Printf("[SkillService] Error reading account directory: %v", err)
} else {
for _, entry := range entries {
if !entry.IsDir() || entry.Name() == "admin" {
continue
}
userSkillsPath := filepath.Join(accountPath, entry.Name(), "skills")
if _, err := os.Stat(userSkillsPath); err == nil {
log.Printf("[SkillService] Scanning user skills for %s from: %s", entry.Name(), userSkillsPath)
userSkills, err := s.scanSkillsDirectory(userSkillsPath, "user")
if err != nil {
log.Printf("[SkillService] Error scanning user skills for %s: %v", entry.Name(), err)
} else {
log.Printf("[SkillService] Found %d user skills for %s", len(userSkills), entry.Name())
// 批量插入
if err := s.skillRepo.UpsertBatch(userSkills); err != nil {
log.Printf("[SkillService] Error saving user skills for %s: %v", entry.Name(), err)
}
}
}
}
}
log.Println("[SkillService] Skills initialized successfully")
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,
Status: "active",
}
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
}