From 6ffd084135c0593c9541a0ae0746a4481c509a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:45:19 +0800 Subject: [PATCH] Add files via upload --- internal/handler/skills.go | 15 +++-- internal/skills/manager.go | 113 ++++++++++++++++++++++++------------- 2 files changed, 83 insertions(+), 45 deletions(-) diff --git a/internal/handler/skills.go b/internal/handler/skills.go index 9ea67202..fececa14 100644 --- a/internal/handler/skills.go +++ b/internal/handler/skills.go @@ -224,9 +224,9 @@ func (h *SkillsHandler) GetSkillBoundRoles(c *gin.Context) { boundRoles := h.getRolesBoundToSkill(skillName) c.JSON(http.StatusOK, gin.H{ - "skill": skillName, - "bound_roles": boundRoles, - "bound_count": len(boundRoles), + "skill": skillName, + "bound_roles": boundRoles, + "bound_count": len(boundRoles), }) } @@ -323,6 +323,7 @@ func (h *SkillsHandler) CreateSkill(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "创建skill文件失败: " + err.Error()}) return } + h.manager.InvalidateSkill(req.Name) h.logger.Info("创建skill成功", zap.String("skill", req.Name)) c.JSON(http.StatusOK, gin.H{ @@ -443,6 +444,7 @@ func (h *SkillsHandler) UpdateSkill(c *gin.Context) { if skillFile != targetFile { os.Remove(skillFile) } + h.manager.InvalidateSkill(skillName) h.logger.Info("更新skill成功", zap.String("skill", skillName)) c.JSON(http.StatusOK, gin.H{ @@ -461,8 +463,8 @@ func (h *SkillsHandler) DeleteSkill(c *gin.Context) { // 检查是否有角色绑定了该skill,如果有则自动移除绑定 affectedRoles := h.removeSkillFromRoles(skillName) if len(affectedRoles) > 0 { - h.logger.Info("从角色中移除skill绑定", - zap.String("skill", skillName), + h.logger.Info("从角色中移除skill绑定", + zap.String("skill", skillName), zap.Strings("roles", affectedRoles)) } @@ -483,10 +485,11 @@ func (h *SkillsHandler) DeleteSkill(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "删除skill失败: " + err.Error()}) return } + h.manager.InvalidateSkill(skillName) responseMsg := "skill已删除" if len(affectedRoles) > 0 { - responseMsg = fmt.Sprintf("skill已删除,已自动从 %d 个角色中移除绑定: %s", + responseMsg = fmt.Sprintf("skill已删除,已自动从 %d 个角色中移除绑定: %s", len(affectedRoles), strings.Join(affectedRoles, ", ")) } diff --git a/internal/skills/manager.go b/internal/skills/manager.go index 4f3c78bd..d49d21cc 100644 --- a/internal/skills/manager.go +++ b/internal/skills/manager.go @@ -14,8 +14,14 @@ import ( type Manager struct { skillsDir string logger *zap.Logger - skills map[string]*Skill // 缓存已加载的skills - mu sync.RWMutex // 保护skills map的并发访问 + skills map[string]*cachedSkill // 缓存已加载的skills(含文件状态) + mu sync.RWMutex // 保护skills map的并发访问 +} + +type cachedSkill struct { + skill *Skill + filePath string + modTime int64 } // Skill Skill定义 @@ -31,49 +37,43 @@ func NewManager(skillsDir string, logger *zap.Logger) *Manager { return &Manager{ skillsDir: skillsDir, logger: logger, - skills: make(map[string]*Skill), + skills: make(map[string]*cachedSkill), } } // LoadSkill 加载单个skill func (m *Manager) LoadSkill(skillName string) (*Skill, error) { - // 先尝试读锁检查缓存 - m.mu.RLock() - if skill, exists := m.skills[skillName]; exists { - m.mu.RUnlock() - return skill, nil - } - m.mu.RUnlock() - // 构建skill路径 skillPath := filepath.Join(m.skillsDir, skillName) - + // 检查目录是否存在 if _, err := os.Stat(skillPath); os.IsNotExist(err) { + m.InvalidateSkill(skillName) return nil, fmt.Errorf("skill %s not found", skillName) } - // 查找SKILL.md文件 - skillFile := filepath.Join(skillPath, "SKILL.md") - if _, err := os.Stat(skillFile); os.IsNotExist(err) { - // 尝试其他可能的文件名 - alternatives := []string{ - filepath.Join(skillPath, "skill.md"), - filepath.Join(skillPath, "README.md"), - filepath.Join(skillPath, "readme.md"), - } - found := false - for _, alt := range alternatives { - if _, err := os.Stat(alt); err == nil { - skillFile = alt - found = true - break - } - } - if !found { - return nil, fmt.Errorf("skill file not found for %s", skillName) - } + // 查找skill文件并读取文件状态 + skillFile, err := m.resolveSkillFile(skillPath) + if err != nil { + m.InvalidateSkill(skillName) + return nil, err } + fileInfo, err := os.Stat(skillFile) + if err != nil { + m.InvalidateSkill(skillName) + return nil, fmt.Errorf("failed to stat skill file: %w", err) + } + modTime := fileInfo.ModTime().UnixNano() + + // 先尝试读锁命中缓存(文件路径和修改时间都未变化) + m.mu.RLock() + if cached, exists := m.skills[skillName]; exists && + cached.filePath == skillFile && + cached.modTime == modTime { + m.mu.RUnlock() + return cached.skill, nil + } + m.mu.RUnlock() // 读取skill文件 content, err := os.ReadFile(skillFile) @@ -83,15 +83,14 @@ func (m *Manager) LoadSkill(skillName string) (*Skill, error) { // 解析skill内容 skill := m.parseSkillContent(string(content), skillName, skillPath) - - // 使用写锁缓存skill(双重检查,避免重复加载) + + // 使用写锁更新缓存 m.mu.Lock() - // 再次检查,可能其他goroutine已经加载了 - if existing, exists := m.skills[skillName]; exists { - m.mu.Unlock() - return existing, nil + m.skills[skillName] = &cachedSkill{ + skill: skill, + filePath: skillFile, + modTime: modTime, } - m.skills[skillName] = skill m.mu.Unlock() return skill, nil @@ -161,6 +160,42 @@ func (m *Manager) ListSkills() ([]string, error) { return skills, nil } +func (m *Manager) resolveSkillFile(skillPath string) (string, error) { + // 优先标准文件名 + skillFile := filepath.Join(skillPath, "SKILL.md") + if _, err := os.Stat(skillFile); err == nil { + return skillFile, nil + } + + // 兼容历史文件名 + alternatives := []string{ + filepath.Join(skillPath, "skill.md"), + filepath.Join(skillPath, "README.md"), + filepath.Join(skillPath, "readme.md"), + } + for _, alt := range alternatives { + if _, err := os.Stat(alt); err == nil { + return alt, nil + } + } + + return "", fmt.Errorf("skill file not found for %s", filepath.Base(skillPath)) +} + +// InvalidateSkill 使指定skill缓存失效 +func (m *Manager) InvalidateSkill(skillName string) { + m.mu.Lock() + delete(m.skills, skillName) + m.mu.Unlock() +} + +// InvalidateAll 清空全部skill缓存 +func (m *Manager) InvalidateAll() { + m.mu.Lock() + m.skills = make(map[string]*cachedSkill) + m.mu.Unlock() +} + // parseSkillContent 解析skill内容 // 支持YAML front matter格式,类似goskills func (m *Manager) parseSkillContent(content, skillName, skillPath string) *Skill {