From 45f4b52353af54400dfd277834cd6638e56a2c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 15 Jan 2026 23:20:26 +0800 Subject: [PATCH] Add files via upload --- internal/app/app.go | 1 - internal/handler/skills.go | 211 +------------------------------------ web/static/css/style.css | 154 +++++++++++---------------- web/static/js/skills.js | 106 +++++-------------- 4 files changed, 87 insertions(+), 385 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 0915af28..5debd7cd 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -700,7 +700,6 @@ func setupRoutes( protected.GET("/skills/stats", skillsHandler.GetSkillStats) protected.DELETE("/skills/stats", skillsHandler.ClearSkillStats) protected.GET("/skills/:name", skillsHandler.GetSkill) - protected.GET("/skills/:name/bound-roles", skillsHandler.GetSkillBoundRoles) protected.POST("/skills", skillsHandler.CreateSkill) protected.PUT("/skills/:name", skillsHandler.UpdateSkill) protected.DELETE("/skills/:name", skillsHandler.DeleteSkill) diff --git a/internal/handler/skills.go b/internal/handler/skills.go index 9ea67202..4870f8c6 100644 --- a/internal/handler/skills.go +++ b/internal/handler/skills.go @@ -5,7 +5,6 @@ import ( "net/http" "os" "path/filepath" - "regexp" "strings" "cyberstrike-ai/internal/config" @@ -14,7 +13,6 @@ import ( "github.com/gin-gonic/gin" "go.uber.org/zap" - "gopkg.in/yaml.v3" ) // SkillsHandler Skills处理器 @@ -52,7 +50,7 @@ func (h *SkillsHandler) GetSkills(c *gin.Context) { // 搜索参数 searchKeyword := strings.TrimSpace(c.Query("search")) - + // 先加载所有skills的详细信息用于搜索过滤 allSkillsInfo := make([]map[string]interface{}, 0, len(skillList)) for _, skillName := range skillList { @@ -107,7 +105,7 @@ func (h *SkillsHandler) GetSkills(c *gin.Context) { 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) { @@ -162,6 +160,7 @@ func (h *SkillsHandler) GetSkills(c *gin.Context) { }) } + // GetSkill 获取单个skill的详细信息 func (h *SkillsHandler) GetSkill(c *gin.Context) { skillName := c.Param("name") @@ -214,49 +213,6 @@ func (h *SkillsHandler) GetSkill(c *gin.Context) { }) } -// 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 { @@ -458,14 +414,6 @@ func (h *SkillsHandler) DeleteSkill(c *gin.Context) { 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 == "" { @@ -484,16 +432,9 @@ func (h *SkillsHandler) DeleteSkill(c *gin.Context) { 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, + "message": "skill已删除", }) } @@ -619,150 +560,6 @@ func (h *SkillsHandler) ClearSkillStatsByName(c *gin.Context) { }) } -// 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 { diff --git a/web/static/css/style.css b/web/static/css/style.css index 18f2b716..396168d0 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -3272,175 +3272,156 @@ header { flex-direction: column; height: 100%; position: relative; - overflow: hidden; } .skills-list-with-pagination { flex: 1; overflow-y: auto; - overflow-x: hidden; + padding-bottom: 90px; /* 为固定分页栏留出空间 */ min-height: 0; - /* 为分页组件预留空间,确保视觉连接自然 */ - padding-bottom: 0; } .pagination-fixed { + position: sticky; + bottom: 0; + z-index: 10; background: var(--bg-primary); + border-top: 1px solid var(--border-color); + box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08); margin-top: 0; - padding: 0; - /* 确保分页组件宽度与内容区域一致,不包括滚动条 */ - width: 100%; - box-sizing: border-box; - /* 确保分页组件不延伸到滚动条区域 */ - overflow: hidden; - /* 当列表有滚动条时,分页组件应该与内容区域对齐 */ - position: relative; - /* 添加四个角的圆角,与上方卡片保持一致 */ - border-radius: 8px; + padding: 16px 24px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); } .pagination-fixed .pagination { margin-top: 0; - border-top: 1px solid var(--border-color); - padding: 16px 20px; - background: var(--bg-primary); + border-top: none; + padding: 0; + background: transparent; + border-radius: 12px; justify-content: space-between; align-items: center; - gap: 20px; + gap: 24px; flex-wrap: wrap; - /* 确保分页内容与列表内容对齐 */ - width: 100%; - box-sizing: border-box; - /* 柔和的顶部边框,与列表自然分离 */ - border-top-color: rgba(233, 236, 239, 0.6); - /* 添加四个角的圆角,与上方卡片保持一致 */ - border-radius: 8px; } -/* 左侧:信息显示和每页数量选择器 - 更自然的设计 */ +/* 左侧:信息显示 */ .pagination-fixed .pagination-info { font-size: 0.875rem; color: var(--text-secondary); + font-weight: 500; + white-space: nowrap; display: flex; align-items: center; - gap: 20px; - flex-wrap: wrap; - /* 去掉背景色和边框,更自然 */ - padding: 0; - background: transparent; - border-radius: 0; } .pagination-fixed .pagination-info span { color: var(--text-primary); - white-space: nowrap; - font-weight: 400; } -.pagination-fixed .pagination-info .pagination-page-size { +/* 中间:每页数量选择器 */ +.pagination-fixed .pagination-page-size { display: flex; align-items: center; gap: 8px; font-size: 0.875rem; color: var(--text-secondary); - font-weight: 400; } -.pagination-fixed .pagination-info .pagination-page-size select { - padding: 6px 10px; - border-radius: 6px; +.pagination-fixed .pagination-page-size label { + font-weight: 500; + white-space: nowrap; +} + +.pagination-fixed .pagination-page-size select { + padding: 6px 12px; border: 1px solid var(--border-color); - background: var(--bg-primary); + border-radius: 8px; + background: var(--bg-secondary); color: var(--text-primary); font-size: 0.875rem; cursor: pointer; transition: all 0.2s ease; min-width: 70px; font-weight: 500; - /* 更柔和的边框 */ - border-color: rgba(233, 236, 239, 0.8); } -.pagination-fixed .pagination-info .pagination-page-size select:focus { +.pagination-fixed .pagination-page-size select:hover { + border-color: var(--accent-color); + background: var(--bg-tertiary); +} + +.pagination-fixed .pagination-page-size select:focus { outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); - background: var(--bg-primary); } -.pagination-fixed .pagination-info .pagination-page-size select:hover { - border-color: var(--accent-color); - background: var(--bg-secondary); -} - -/* 右侧:分页按钮组 - 更统一的设计 */ +/* 右侧:分页按钮组 */ .pagination-fixed .pagination-controls { display: flex; align-items: center; - gap: 6px; + gap: 8px; flex-wrap: wrap; } .pagination-fixed .pagination-controls .btn-secondary { - padding: 7px 14px; + padding: 8px 16px; font-size: 0.875rem; - min-width: auto; - transition: all 0.2s ease; font-weight: 500; border-radius: 8px; - /* 更柔和的边框 */ - border-color: rgba(233, 236, 239, 0.8); + min-width: auto; + transition: all 0.2s ease; } .pagination-fixed .pagination-controls .btn-secondary:hover:not(:disabled) { - background: var(--bg-tertiary); - border-color: var(--accent-color); - color: var(--accent-color); transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 102, 255, 0.1); -} - -.pagination-fixed .pagination-controls .btn-secondary:active:not(:disabled) { - transform: translateY(0); - box-shadow: 0 1px 2px rgba(0, 102, 255, 0.08); + box-shadow: 0 2px 4px rgba(0, 102, 255, 0.15); } .pagination-fixed .pagination-controls .btn-secondary:disabled { - opacity: 0.4; + opacity: 0.5; cursor: not-allowed; - background: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-muted); + transform: none; + box-shadow: none; } .pagination-fixed .pagination-page { font-size: 0.875rem; color: var(--text-secondary); + font-weight: 500; padding: 0 12px; white-space: nowrap; - font-weight: 400; } /* 响应式优化 */ @media (max-width: 768px) { + .pagination-fixed { + padding: 12px 16px; + } + .pagination-fixed .pagination { flex-direction: column; - gap: 16px; + gap: 12px; align-items: stretch; - padding: 16px; } .pagination-fixed .pagination-info { + width: 100%; + text-align: center; + justify-content: center; + } + + .pagination-fixed .pagination-page-size { width: 100%; justify-content: center; - gap: 16px; } .pagination-fixed .pagination-controls { width: 100%; justify-content: center; flex-wrap: wrap; - gap: 6px; } .pagination-fixed .pagination-controls .btn-secondary { @@ -3448,6 +3429,10 @@ header { min-width: 60px; max-width: 120px; } + + .skills-list-with-pagination { + padding-bottom: 140px; /* 移动端需要更多空间 */ + } } .pagination-info { @@ -3952,27 +3937,6 @@ header { width: 18px; height: 18px; accent-color: var(--accent-color); - margin: 0; - vertical-align: middle; - display: inline-block; -} - -/* 确保复选框单元格垂直居中 */ -.monitor-table td:first-child, -.monitor-table th:first-child { - text-align: center; - vertical-align: middle; -} - -/* 统一表头复选框大小 */ -#monitor-select-all { - width: 18px; - height: 18px; - cursor: pointer; - accent-color: var(--accent-color); - margin: 0; - vertical-align: middle; - display: inline-block; } /* 移除第一列的特殊样式,让表格更灵活 */ diff --git a/web/static/js/skills.js b/web/static/js/skills.js index 79de38b1..ca2fe1d9 100644 --- a/web/static/js/skills.js +++ b/web/static/js/skills.js @@ -165,76 +165,45 @@ function renderSkillsPagination() { } // 计算显示范围 - const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1; - const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total); + const start = (currentPage - 1) * pageSize + 1; + const end = Math.min(currentPage * pageSize, total); let paginationHTML = ''; paginationContainer.innerHTML = paginationHTML; - - // 确保分页组件与列表内容区域对齐(不包括滚动条) - function alignPaginationWidth() { - const skillsList = document.getElementById('skills-list'); - if (skillsList && paginationContainer) { - // 获取列表的实际内容宽度(不包括滚动条) - const listClientWidth = skillsList.clientWidth; // 可视区域宽度(不包括滚动条) - const listScrollHeight = skillsList.scrollHeight; // 内容总高度 - const listClientHeight = skillsList.clientHeight; // 可视区域高度 - const hasScrollbar = listScrollHeight > listClientHeight; - - // 如果列表有垂直滚动条,分页组件应该与列表内容区域对齐(clientWidth) - // 如果没有滚动条,使用100%宽度 - if (hasScrollbar) { - // 分页组件应该与列表内容区域对齐,不包括滚动条 - paginationContainer.style.width = `${listClientWidth}px`; - } else { - // 如果没有滚动条,使用100%宽度 - paginationContainer.style.width = '100%'; - } - } - } - - // 立即执行一次 - alignPaginationWidth(); - - // 监听窗口大小变化和列表内容变化 - const resizeObserver = new ResizeObserver(() => { - alignPaginationWidth(); - }); - - const skillsList = document.getElementById('skills-list'); - if (skillsList) { - resizeObserver.observe(skillsList); - } } // 改变每页显示数量 @@ -492,27 +461,7 @@ async function saveSkill() { // 删除skill async function deleteSkill(skillName) { - // 先检查是否有角色绑定了该skill - let boundRoles = []; - try { - const checkResponse = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}/bound-roles`); - if (checkResponse.ok) { - const checkData = await checkResponse.json(); - boundRoles = checkData.bound_roles || []; - } - } catch (error) { - console.warn('检查skill绑定失败:', error); - // 如果检查失败,继续执行删除流程 - } - - // 构建确认消息 - let confirmMessage = `确定要删除skill "${skillName}" 吗?此操作不可恢复。`; - if (boundRoles.length > 0) { - const rolesList = boundRoles.join('、'); - confirmMessage = `确定要删除skill "${skillName}" 吗?\n\n⚠️ 该skill当前已被以下 ${boundRoles.length} 个角色绑定:\n${rolesList}\n\n删除后,系统将自动从这些角色中移除该skill的绑定。\n\n此操作不可恢复,是否继续?`; - } - - if (!confirm(confirmMessage)) { + if (!confirm(`确定要删除skill "${skillName}" 吗?此操作不可恢复。`)) { return; } @@ -526,14 +475,7 @@ async function deleteSkill(skillName) { throw new Error(error.error || '删除skill失败'); } - const data = await response.json(); - let successMessage = 'skill已删除'; - if (data.affected_roles && data.affected_roles.length > 0) { - const rolesList = data.affected_roles.join('、'); - successMessage = `skill已删除,已自动从 ${data.affected_roles.length} 个角色中移除绑定:${rolesList}`; - } - showNotification(successMessage, 'success'); - + showNotification('skill已删除', 'success'); // 如果当前页没有数据了,回到上一页 const currentPage = skillsPagination.currentPage; const totalAfterDelete = skillsPagination.total - 1; @@ -644,7 +586,7 @@ function renderSkillsMonitor() { - + @@ -662,7 +604,7 @@ function renderSkillsMonitor() { return ` - +
Skill名称Skill名称 总调用 成功 失败
${escapeHtml(stat.skill_name || '')}${escapeHtml(stat.skill_name || '')} ${totalCalls} ${successCalls} ${failedCalls}