mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-03-31 08:19:54 +02:00
779 lines
22 KiB
Go
779 lines
22 KiB
Go
package handler
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"cyberstrike-ai/internal/config"
|
||
"cyberstrike-ai/internal/database"
|
||
"cyberstrike-ai/internal/skills"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
// SkillsHandler Skills处理器
|
||
type SkillsHandler struct {
|
||
manager *skills.Manager
|
||
config *config.Config
|
||
configPath string
|
||
logger *zap.Logger
|
||
db *database.DB // 数据库连接(用于获取调用统计)
|
||
}
|
||
|
||
// NewSkillsHandler 创建新的Skills处理器
|
||
func NewSkillsHandler(manager *skills.Manager, cfg *config.Config, configPath string, logger *zap.Logger) *SkillsHandler {
|
||
return &SkillsHandler{
|
||
manager: manager,
|
||
config: cfg,
|
||
configPath: configPath,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// SetDB 设置数据库连接(用于获取调用统计)
|
||
func (h *SkillsHandler) SetDB(db *database.DB) {
|
||
h.db = db
|
||
}
|
||
|
||
// GetSkills 获取所有skills列表(支持分页和搜索)
|
||
func (h *SkillsHandler) GetSkills(c *gin.Context) {
|
||
skillList, err := h.manager.ListSkills()
|
||
if err != nil {
|
||
h.logger.Error("获取skills列表失败", zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 搜索参数
|
||
searchKeyword := strings.TrimSpace(c.Query("search"))
|
||
|
||
// 先加载所有skills的详细信息用于搜索过滤
|
||
allSkillsInfo := make([]map[string]interface{}, 0, len(skillList))
|
||
for _, skillName := range skillList {
|
||
skill, err := h.manager.LoadSkill(skillName)
|
||
if err != nil {
|
||
h.logger.Warn("加载skill失败", zap.String("skill", skillName), zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
// 获取文件信息
|
||
skillPath := skill.Path
|
||
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"),
|
||
}
|
||
for _, alt := range alternatives {
|
||
if _, err := os.Stat(alt); err == nil {
|
||
skillFile = alt
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
fileInfo, _ := os.Stat(skillFile)
|
||
var fileSize int64
|
||
var modTime string
|
||
if fileInfo != nil {
|
||
fileSize = fileInfo.Size()
|
||
modTime = fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
skillInfo := map[string]interface{}{
|
||
"name": skill.Name,
|
||
"description": skill.Description,
|
||
"path": skill.Path,
|
||
"file_size": fileSize,
|
||
"mod_time": modTime,
|
||
}
|
||
allSkillsInfo = append(allSkillsInfo, skillInfo)
|
||
}
|
||
|
||
// 如果有搜索关键词,进行过滤
|
||
filteredSkillsInfo := allSkillsInfo
|
||
if searchKeyword != "" {
|
||
keywordLower := strings.ToLower(searchKeyword)
|
||
filteredSkillsInfo = make([]map[string]interface{}, 0)
|
||
for _, skillInfo := range allSkillsInfo {
|
||
name := strings.ToLower(fmt.Sprintf("%v", skillInfo["name"]))
|
||
description := strings.ToLower(fmt.Sprintf("%v", skillInfo["description"]))
|
||
path := strings.ToLower(fmt.Sprintf("%v", skillInfo["path"]))
|
||
|
||
if strings.Contains(name, keywordLower) ||
|
||
strings.Contains(description, keywordLower) ||
|
||
strings.Contains(path, keywordLower) {
|
||
filteredSkillsInfo = append(filteredSkillsInfo, skillInfo)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分页参数
|
||
limit := 20 // 默认每页20条
|
||
offset := 0
|
||
if limitStr := c.Query("limit"); limitStr != "" {
|
||
if parsed, err := parseInt(limitStr); err == nil && parsed > 0 {
|
||
// 允许更大的limit用于搜索场景,但设置一个合理的上限(10000)
|
||
if parsed <= 10000 {
|
||
limit = parsed
|
||
} else {
|
||
limit = 10000
|
||
}
|
||
}
|
||
}
|
||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||
if parsed, err := parseInt(offsetStr); err == nil && parsed >= 0 {
|
||
offset = parsed
|
||
}
|
||
}
|
||
|
||
// 计算分页范围
|
||
total := len(filteredSkillsInfo)
|
||
start := offset
|
||
end := offset + limit
|
||
if start > total {
|
||
start = total
|
||
}
|
||
if end > total {
|
||
end = total
|
||
}
|
||
|
||
// 获取当前页的skill列表
|
||
var paginatedSkillsInfo []map[string]interface{}
|
||
if start < end {
|
||
paginatedSkillsInfo = filteredSkillsInfo[start:end]
|
||
} else {
|
||
paginatedSkillsInfo = []map[string]interface{}{}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"skills": paginatedSkillsInfo,
|
||
"total": total,
|
||
"limit": limit,
|
||
"offset": offset,
|
||
})
|
||
}
|
||
|
||
// GetSkill 获取单个skill的详细信息
|
||
func (h *SkillsHandler) GetSkill(c *gin.Context) {
|
||
skillName := c.Param("name")
|
||
if skillName == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
|
||
return
|
||
}
|
||
|
||
skill, err := h.manager.LoadSkill(skillName)
|
||
if err != nil {
|
||
h.logger.Warn("加载skill失败", zap.String("skill", skillName), zap.Error(err))
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "skill不存在: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 获取文件信息
|
||
skillPath := skill.Path
|
||
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"),
|
||
}
|
||
for _, alt := range alternatives {
|
||
if _, err := os.Stat(alt); err == nil {
|
||
skillFile = alt
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
fileInfo, _ := os.Stat(skillFile)
|
||
var fileSize int64
|
||
var modTime string
|
||
if fileInfo != nil {
|
||
fileSize = fileInfo.Size()
|
||
modTime = fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"skill": map[string]interface{}{
|
||
"name": skill.Name,
|
||
"description": skill.Description,
|
||
"content": skill.Content,
|
||
"path": skill.Path,
|
||
"file_size": fileSize,
|
||
"mod_time": modTime,
|
||
},
|
||
})
|
||
}
|
||
|
||
// GetSkillBoundRoles 获取绑定指定skill的角色列表
|
||
func (h *SkillsHandler) GetSkillBoundRoles(c *gin.Context) {
|
||
skillName := c.Param("name")
|
||
if skillName == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
|
||
return
|
||
}
|
||
|
||
boundRoles := h.getRolesBoundToSkill(skillName)
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"skill": skillName,
|
||
"bound_roles": boundRoles,
|
||
"bound_count": len(boundRoles),
|
||
})
|
||
}
|
||
|
||
// getRolesBoundToSkill 获取绑定指定skill的角色列表(不修改配置)
|
||
func (h *SkillsHandler) getRolesBoundToSkill(skillName string) []string {
|
||
if h.config.Roles == nil {
|
||
return []string{}
|
||
}
|
||
|
||
boundRoles := make([]string, 0)
|
||
for roleName, role := range h.config.Roles {
|
||
// 确保角色名称正确设置
|
||
if role.Name == "" {
|
||
role.Name = roleName
|
||
}
|
||
|
||
// 检查角色的Skills列表中是否包含该skill
|
||
if len(role.Skills) > 0 {
|
||
for _, skill := range role.Skills {
|
||
if skill == skillName {
|
||
boundRoles = append(boundRoles, roleName)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return boundRoles
|
||
}
|
||
|
||
// CreateSkill 创建新skill
|
||
func (h *SkillsHandler) CreateSkill(c *gin.Context) {
|
||
var req struct {
|
||
Name string `json:"name" binding:"required"`
|
||
Description string `json:"description"`
|
||
Content string `json:"content" binding:"required"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 验证skill名称(只允许字母、数字、连字符和下划线)
|
||
if !isValidSkillName(req.Name) {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称只能包含字母、数字、连字符和下划线"})
|
||
return
|
||
}
|
||
|
||
// 获取skills目录
|
||
skillsDir := h.config.SkillsDir
|
||
if skillsDir == "" {
|
||
skillsDir = "skills"
|
||
}
|
||
configDir := filepath.Dir(h.configPath)
|
||
if !filepath.IsAbs(skillsDir) {
|
||
skillsDir = filepath.Join(configDir, skillsDir)
|
||
}
|
||
|
||
// 创建skill目录
|
||
skillDir := filepath.Join(skillsDir, req.Name)
|
||
if err := os.MkdirAll(skillDir, 0755); err != nil {
|
||
h.logger.Error("创建skill目录失败", zap.String("skill", req.Name), zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建skill目录失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 检查是否已存在
|
||
skillFile := filepath.Join(skillDir, "SKILL.md")
|
||
if _, err := os.Stat(skillFile); err == nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "skill已存在"})
|
||
return
|
||
}
|
||
|
||
// 构建SKILL.md内容
|
||
var content strings.Builder
|
||
content.WriteString("---\n")
|
||
content.WriteString(fmt.Sprintf("name: %s\n", req.Name))
|
||
if req.Description != "" {
|
||
// 如果描述包含特殊字符,需要加引号
|
||
desc := req.Description
|
||
if strings.Contains(desc, ":") || strings.Contains(desc, "\n") {
|
||
desc = fmt.Sprintf(`"%s"`, strings.ReplaceAll(desc, `"`, `\"`))
|
||
}
|
||
content.WriteString(fmt.Sprintf("description: %s\n", desc))
|
||
}
|
||
content.WriteString("version: 1.0.0\n")
|
||
content.WriteString("---\n\n")
|
||
content.WriteString(req.Content)
|
||
|
||
// 写入文件
|
||
if err := os.WriteFile(skillFile, []byte(content.String()), 0644); err != nil {
|
||
h.logger.Error("创建skill文件失败", zap.String("skill", req.Name), zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建skill文件失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
h.logger.Info("创建skill成功", zap.String("skill", req.Name))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "skill已创建",
|
||
"skill": map[string]interface{}{
|
||
"name": req.Name,
|
||
"path": skillDir,
|
||
},
|
||
})
|
||
}
|
||
|
||
// UpdateSkill 更新skill
|
||
func (h *SkillsHandler) UpdateSkill(c *gin.Context) {
|
||
skillName := c.Param("name")
|
||
if skillName == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Description string `json:"description"`
|
||
Content string `json:"content" binding:"required"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 获取skills目录
|
||
skillsDir := h.config.SkillsDir
|
||
if skillsDir == "" {
|
||
skillsDir = "skills"
|
||
}
|
||
configDir := filepath.Dir(h.configPath)
|
||
if !filepath.IsAbs(skillsDir) {
|
||
skillsDir = filepath.Join(configDir, skillsDir)
|
||
}
|
||
|
||
// 查找skill文件
|
||
skillDir := filepath.Join(skillsDir, skillName)
|
||
skillFile := filepath.Join(skillDir, "SKILL.md")
|
||
if _, err := os.Stat(skillFile); os.IsNotExist(err) {
|
||
alternatives := []string{
|
||
filepath.Join(skillDir, "skill.md"),
|
||
filepath.Join(skillDir, "README.md"),
|
||
filepath.Join(skillDir, "readme.md"),
|
||
}
|
||
found := false
|
||
for _, alt := range alternatives {
|
||
if _, err := os.Stat(alt); err == nil {
|
||
skillFile = alt
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "skill不存在"})
|
||
return
|
||
}
|
||
}
|
||
|
||
// 读取现有文件以保留front matter中的name
|
||
existingContent, err := os.ReadFile(skillFile)
|
||
if err != nil {
|
||
h.logger.Error("读取skill文件失败", zap.String("skill", skillName), zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取skill文件失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 解析现有内容,提取name
|
||
existingName := skillName
|
||
contentStr := string(existingContent)
|
||
if strings.HasPrefix(contentStr, "---") {
|
||
parts := strings.SplitN(contentStr, "---", 3)
|
||
if len(parts) >= 2 {
|
||
frontMatter := parts[1]
|
||
lines := strings.Split(frontMatter, "\n")
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "name:") {
|
||
name := strings.TrimSpace(strings.TrimPrefix(line, "name:"))
|
||
name = strings.Trim(name, `"'`)
|
||
if name != "" {
|
||
existingName = name
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 构建新的SKILL.md内容
|
||
var newContent strings.Builder
|
||
newContent.WriteString("---\n")
|
||
newContent.WriteString(fmt.Sprintf("name: %s\n", existingName))
|
||
if req.Description != "" {
|
||
// 如果描述包含特殊字符,需要加引号
|
||
desc := req.Description
|
||
if strings.Contains(desc, ":") || strings.Contains(desc, "\n") {
|
||
desc = fmt.Sprintf(`"%s"`, strings.ReplaceAll(desc, `"`, `\"`))
|
||
}
|
||
newContent.WriteString(fmt.Sprintf("description: %s\n", desc))
|
||
}
|
||
newContent.WriteString("version: 1.0.0\n")
|
||
newContent.WriteString("---\n\n")
|
||
newContent.WriteString(req.Content)
|
||
|
||
// 写入文件(统一使用SKILL.md)
|
||
targetFile := filepath.Join(skillDir, "SKILL.md")
|
||
if err := os.WriteFile(targetFile, []byte(newContent.String()), 0644); err != nil {
|
||
h.logger.Error("更新skill文件失败", zap.String("skill", skillName), zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新skill文件失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 如果原文件不是SKILL.md,删除旧文件
|
||
if skillFile != targetFile {
|
||
os.Remove(skillFile)
|
||
}
|
||
|
||
h.logger.Info("更新skill成功", zap.String("skill", skillName))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "skill已更新",
|
||
})
|
||
}
|
||
|
||
// DeleteSkill 删除skill
|
||
func (h *SkillsHandler) DeleteSkill(c *gin.Context) {
|
||
skillName := c.Param("name")
|
||
if skillName == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
|
||
return
|
||
}
|
||
|
||
// 检查是否有角色绑定了该skill,如果有则自动移除绑定
|
||
affectedRoles := h.removeSkillFromRoles(skillName)
|
||
if len(affectedRoles) > 0 {
|
||
h.logger.Info("从角色中移除skill绑定",
|
||
zap.String("skill", skillName),
|
||
zap.Strings("roles", affectedRoles))
|
||
}
|
||
|
||
// 获取skills目录
|
||
skillsDir := h.config.SkillsDir
|
||
if skillsDir == "" {
|
||
skillsDir = "skills"
|
||
}
|
||
configDir := filepath.Dir(h.configPath)
|
||
if !filepath.IsAbs(skillsDir) {
|
||
skillsDir = filepath.Join(configDir, skillsDir)
|
||
}
|
||
|
||
// 删除skill目录
|
||
skillDir := filepath.Join(skillsDir, skillName)
|
||
if err := os.RemoveAll(skillDir); err != nil {
|
||
h.logger.Error("删除skill失败", zap.String("skill", skillName), zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除skill失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
responseMsg := "skill已删除"
|
||
if len(affectedRoles) > 0 {
|
||
responseMsg = fmt.Sprintf("skill已删除,已自动从 %d 个角色中移除绑定: %s",
|
||
len(affectedRoles), strings.Join(affectedRoles, ", "))
|
||
}
|
||
|
||
h.logger.Info("删除skill成功", zap.String("skill", skillName))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": responseMsg,
|
||
"affected_roles": affectedRoles,
|
||
})
|
||
}
|
||
|
||
// GetSkillStats 获取skills调用统计信息
|
||
func (h *SkillsHandler) GetSkillStats(c *gin.Context) {
|
||
skillList, err := h.manager.ListSkills()
|
||
if err != nil {
|
||
h.logger.Error("获取skills列表失败", zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 获取skills目录
|
||
skillsDir := h.config.SkillsDir
|
||
if skillsDir == "" {
|
||
skillsDir = "skills"
|
||
}
|
||
configDir := filepath.Dir(h.configPath)
|
||
if !filepath.IsAbs(skillsDir) {
|
||
skillsDir = filepath.Join(configDir, skillsDir)
|
||
}
|
||
|
||
// 从数据库加载调用统计
|
||
var skillStatsMap map[string]*database.SkillStats
|
||
if h.db != nil {
|
||
dbStats, err := h.db.LoadSkillStats()
|
||
if err != nil {
|
||
h.logger.Warn("从数据库加载Skills统计信息失败", zap.Error(err))
|
||
skillStatsMap = make(map[string]*database.SkillStats)
|
||
} else {
|
||
skillStatsMap = dbStats
|
||
}
|
||
} else {
|
||
skillStatsMap = make(map[string]*database.SkillStats)
|
||
}
|
||
|
||
// 构建统计信息(包含所有skills,即使没有调用记录)
|
||
statsList := make([]map[string]interface{}, 0, len(skillList))
|
||
totalCalls := 0
|
||
totalSuccess := 0
|
||
totalFailed := 0
|
||
|
||
for _, skillName := range skillList {
|
||
stat, exists := skillStatsMap[skillName]
|
||
if !exists {
|
||
stat = &database.SkillStats{
|
||
SkillName: skillName,
|
||
TotalCalls: 0,
|
||
SuccessCalls: 0,
|
||
FailedCalls: 0,
|
||
}
|
||
}
|
||
|
||
totalCalls += stat.TotalCalls
|
||
totalSuccess += stat.SuccessCalls
|
||
totalFailed += stat.FailedCalls
|
||
|
||
lastCallTimeStr := ""
|
||
if stat.LastCallTime != nil {
|
||
lastCallTimeStr = stat.LastCallTime.Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
statsList = append(statsList, map[string]interface{}{
|
||
"skill_name": stat.SkillName,
|
||
"total_calls": stat.TotalCalls,
|
||
"success_calls": stat.SuccessCalls,
|
||
"failed_calls": stat.FailedCalls,
|
||
"last_call_time": lastCallTimeStr,
|
||
})
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"total_skills": len(skillList),
|
||
"total_calls": totalCalls,
|
||
"total_success": totalSuccess,
|
||
"total_failed": totalFailed,
|
||
"skills_dir": skillsDir,
|
||
"stats": statsList,
|
||
})
|
||
}
|
||
|
||
// ClearSkillStats 清空所有Skills统计信息
|
||
func (h *SkillsHandler) ClearSkillStats(c *gin.Context) {
|
||
if h.db == nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库连接未配置"})
|
||
return
|
||
}
|
||
|
||
if err := h.db.ClearSkillStats(); err != nil {
|
||
h.logger.Error("清空Skills统计信息失败", zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空统计信息失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
h.logger.Info("已清空所有Skills统计信息")
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "已清空所有Skills统计信息",
|
||
})
|
||
}
|
||
|
||
// ClearSkillStatsByName 清空指定skill的统计信息
|
||
func (h *SkillsHandler) ClearSkillStatsByName(c *gin.Context) {
|
||
skillName := c.Param("name")
|
||
if skillName == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
|
||
return
|
||
}
|
||
|
||
if h.db == nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库连接未配置"})
|
||
return
|
||
}
|
||
|
||
if err := h.db.ClearSkillStatsByName(skillName); err != nil {
|
||
h.logger.Error("清空指定skill统计信息失败", zap.String("skill", skillName), zap.Error(err))
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空统计信息失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
h.logger.Info("已清空指定skill统计信息", zap.String("skill", skillName))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": fmt.Sprintf("已清空skill '%s' 的统计信息", skillName),
|
||
})
|
||
}
|
||
|
||
// removeSkillFromRoles 从所有角色中移除指定的skill绑定
|
||
// 返回受影响角色名称列表
|
||
func (h *SkillsHandler) removeSkillFromRoles(skillName string) []string {
|
||
if h.config.Roles == nil {
|
||
return []string{}
|
||
}
|
||
|
||
affectedRoles := make([]string, 0)
|
||
rolesToUpdate := make(map[string]config.RoleConfig)
|
||
|
||
// 遍历所有角色,查找并移除skill绑定
|
||
for roleName, role := range h.config.Roles {
|
||
// 确保角色名称正确设置
|
||
if role.Name == "" {
|
||
role.Name = roleName
|
||
}
|
||
|
||
// 检查角色的Skills列表中是否包含要删除的skill
|
||
if len(role.Skills) > 0 {
|
||
updated := false
|
||
newSkills := make([]string, 0, len(role.Skills))
|
||
for _, skill := range role.Skills {
|
||
if skill != skillName {
|
||
newSkills = append(newSkills, skill)
|
||
} else {
|
||
updated = true
|
||
}
|
||
}
|
||
if updated {
|
||
role.Skills = newSkills
|
||
rolesToUpdate[roleName] = role
|
||
affectedRoles = append(affectedRoles, roleName)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果有角色需要更新,保存到文件
|
||
if len(rolesToUpdate) > 0 {
|
||
// 更新内存中的配置
|
||
for roleName, role := range rolesToUpdate {
|
||
h.config.Roles[roleName] = role
|
||
}
|
||
// 保存更新后的角色配置到文件
|
||
if err := h.saveRolesConfig(); err != nil {
|
||
h.logger.Error("保存角色配置失败", zap.Error(err))
|
||
}
|
||
}
|
||
|
||
return affectedRoles
|
||
}
|
||
|
||
// saveRolesConfig 保存角色配置到文件(从SkillsHandler调用)
|
||
func (h *SkillsHandler) saveRolesConfig() error {
|
||
configDir := filepath.Dir(h.configPath)
|
||
rolesDir := h.config.RolesDir
|
||
if rolesDir == "" {
|
||
rolesDir = "roles" // 默认目录
|
||
}
|
||
|
||
// 如果是相对路径,相对于配置文件所在目录
|
||
if !filepath.IsAbs(rolesDir) {
|
||
rolesDir = filepath.Join(configDir, rolesDir)
|
||
}
|
||
|
||
// 确保目录存在
|
||
if err := os.MkdirAll(rolesDir, 0755); err != nil {
|
||
return fmt.Errorf("创建角色目录失败: %w", err)
|
||
}
|
||
|
||
// 保存每个角色到独立的文件
|
||
if h.config.Roles != nil {
|
||
for roleName, role := range h.config.Roles {
|
||
// 确保角色名称正确设置
|
||
if role.Name == "" {
|
||
role.Name = roleName
|
||
}
|
||
|
||
// 使用角色名称作为文件名(安全化文件名,避免特殊字符)
|
||
safeFileName := sanitizeRoleFileName(role.Name)
|
||
roleFile := filepath.Join(rolesDir, safeFileName+".yaml")
|
||
|
||
// 将角色配置序列化为YAML
|
||
roleData, err := yaml.Marshal(&role)
|
||
if err != nil {
|
||
h.logger.Error("序列化角色配置失败", zap.String("role", roleName), zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
// 处理icon字段:确保包含\U的icon值被引号包围(YAML需要引号才能正确解析Unicode转义)
|
||
roleDataStr := string(roleData)
|
||
if role.Icon != "" && strings.HasPrefix(role.Icon, "\\U") {
|
||
// 匹配 icon: \UXXXXXXXX 格式(没有引号),排除已经有引号的情况
|
||
re := regexp.MustCompile(`(?m)^(icon:\s+)(\\U[0-9A-F]{8})(\s*)$`)
|
||
roleDataStr = re.ReplaceAllString(roleDataStr, `${1}"${2}"${3}`)
|
||
roleData = []byte(roleDataStr)
|
||
}
|
||
|
||
// 写入文件
|
||
if err := os.WriteFile(roleFile, roleData, 0644); err != nil {
|
||
h.logger.Error("保存角色配置文件失败", zap.String("role", roleName), zap.String("file", roleFile), zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
h.logger.Info("角色配置已保存到文件", zap.String("role", roleName), zap.String("file", roleFile))
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// sanitizeRoleFileName 将角色名称转换为安全的文件名
|
||
func sanitizeRoleFileName(name string) string {
|
||
// 替换可能不安全的字符
|
||
replacer := map[rune]string{
|
||
'/': "_",
|
||
'\\': "_",
|
||
':': "_",
|
||
'*': "_",
|
||
'?': "_",
|
||
'"': "_",
|
||
'<': "_",
|
||
'>': "_",
|
||
'|': "_",
|
||
' ': "_",
|
||
}
|
||
|
||
var result []rune
|
||
for _, r := range name {
|
||
if replacement, ok := replacer[r]; ok {
|
||
result = append(result, []rune(replacement)...)
|
||
} else {
|
||
result = append(result, r)
|
||
}
|
||
}
|
||
|
||
fileName := string(result)
|
||
// 如果文件名为空,使用默认名称
|
||
if fileName == "" {
|
||
fileName = "role"
|
||
}
|
||
|
||
return fileName
|
||
}
|
||
|
||
// isValidSkillName 验证skill名称是否有效
|
||
func isValidSkillName(name string) bool {
|
||
if name == "" || len(name) > 100 {
|
||
return false
|
||
}
|
||
// 只允许字母、数字、连字符和下划线
|
||
for _, r := range name {
|
||
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_') {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|