diff --git a/internal/app/app.go b/internal/app/app.go index 5debd7cd..0915af28 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -700,6 +700,7 @@ 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 4870f8c6..9ea67202 100644 --- a/internal/handler/skills.go +++ b/internal/handler/skills.go @@ -5,6 +5,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "strings" "cyberstrike-ai/internal/config" @@ -13,6 +14,7 @@ import ( "github.com/gin-gonic/gin" "go.uber.org/zap" + "gopkg.in/yaml.v3" ) // SkillsHandler Skills处理器 @@ -50,7 +52,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 { @@ -105,7 +107,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) { @@ -160,7 +162,6 @@ func (h *SkillsHandler) GetSkills(c *gin.Context) { }) } - // GetSkill 获取单个skill的详细信息 func (h *SkillsHandler) GetSkill(c *gin.Context) { skillName := c.Param("name") @@ -213,6 +214,49 @@ 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 { @@ -414,6 +458,14 @@ 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 == "" { @@ -432,9 +484,16 @@ 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": "skill已删除", + "message": responseMsg, + "affected_roles": affectedRoles, }) } @@ -560,6 +619,150 @@ 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/internal/skills/manager.go b/internal/skills/manager.go index afc36a31..4f3c78bd 100644 --- a/internal/skills/manager.go +++ b/internal/skills/manager.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "sync" "go.uber.org/zap" ) @@ -14,6 +15,7 @@ type Manager struct { skillsDir string logger *zap.Logger skills map[string]*Skill // 缓存已加载的skills + mu sync.RWMutex // 保护skills map的并发访问 } // Skill Skill定义 @@ -35,10 +37,13 @@ func NewManager(skillsDir string, logger *zap.Logger) *Manager { // 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) @@ -79,8 +84,15 @@ func (m *Manager) LoadSkill(skillName string) (*Skill, error) { // 解析skill内容 skill := m.parseSkillContent(string(content), skillName, skillPath) - // 缓存skill + // 使用写锁缓存skill(双重检查,避免重复加载) + m.mu.Lock() + // 再次检查,可能其他goroutine已经加载了 + if existing, exists := m.skills[skillName]; exists { + m.mu.Unlock() + return existing, nil + } m.skills[skillName] = skill + m.mu.Unlock() return skill, nil } diff --git a/web/static/css/style.css b/web/static/css/style.css index 396168d0..1b4d2b4b 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -3272,156 +3272,175 @@ header { flex-direction: column; height: 100%; position: relative; + overflow: hidden; } .skills-list-with-pagination { flex: 1; overflow-y: auto; - padding-bottom: 90px; /* 为固定分页栏留出空间 */ + overflow-x: hidden; 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: 16px 24px; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + padding: 0; + /* 确保分页组件宽度与内容区域一致,不包括滚动条 */ + width: 100%; + box-sizing: border-box; + /* 确保分页组件不延伸到滚动条区域 */ + overflow: hidden; + /* 当列表有滚动条时,分页组件应该与内容区域对齐 */ + position: relative; + /* 添加四个角的圆角,与上方卡片保持一致 */ + border-radius: 8px; } .pagination-fixed .pagination { margin-top: 0; - border-top: none; - padding: 0; - background: transparent; - border-radius: 12px; + border-top: 1px solid var(--border-color); + padding: 16px 20px; + background: var(--bg-primary); justify-content: space-between; align-items: center; - gap: 24px; + gap: 20px; 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-page-size { +.pagination-fixed .pagination-info .pagination-page-size { display: flex; align-items: center; gap: 8px; font-size: 0.875rem; color: var(--text-secondary); + font-weight: 400; } -.pagination-fixed .pagination-page-size label { - font-weight: 500; - white-space: nowrap; -} - -.pagination-fixed .pagination-page-size select { - padding: 6px 12px; +.pagination-fixed .pagination-info .pagination-page-size select { + padding: 6px 10px; + border-radius: 6px; border: 1px solid var(--border-color); - border-radius: 8px; - background: var(--bg-secondary); + background: var(--bg-primary); 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-page-size select:hover { - border-color: var(--accent-color); - background: var(--bg-tertiary); -} - -.pagination-fixed .pagination-page-size select:focus { +.pagination-fixed .pagination-info .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: 8px; + gap: 6px; flex-wrap: wrap; } .pagination-fixed .pagination-controls .btn-secondary { - padding: 8px 16px; + padding: 7px 14px; font-size: 0.875rem; - font-weight: 500; - border-radius: 8px; min-width: auto; transition: all 0.2s ease; + font-weight: 500; + border-radius: 8px; + /* 更柔和的边框 */ + border-color: rgba(233, 236, 239, 0.8); } .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.15); + 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); } .pagination-fixed .pagination-controls .btn-secondary:disabled { - opacity: 0.5; + opacity: 0.4; cursor: not-allowed; - transform: none; - box-shadow: none; + background: var(--bg-secondary); + border-color: var(--border-color); + color: var(--text-muted); } .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: 12px; + gap: 16px; 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 { @@ -3429,10 +3448,6 @@ header { min-width: 60px; max-width: 120px; } - - .skills-list-with-pagination { - padding-bottom: 140px; /* 移动端需要更多空间 */ - } } .pagination-info { @@ -3937,6 +3952,27 @@ 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; } /* 移除第一列的特殊样式,让表格更灵活 */ @@ -6540,48 +6576,152 @@ header { /* 创建分组模态框 */ .create-group-modal-content { - max-width: 500px; - width: 90vw; + max-width: 640px; + width: 60vw; + margin: 8% auto; +} + +.create-group-modal-content .modal-header { + padding: 12px 20px; + border-bottom: 1px solid #f0f0f0; + background: #ffffff; +} + +.create-group-modal-content .modal-header h2 { + font-size: 1rem; + font-weight: 600; + color: #1a1a1a; + background: none; + -webkit-background-clip: unset; + -webkit-text-fill-color: #1a1a1a; + background-clip: unset; + margin: 0; +} + +.create-group-modal-content .modal-close { + width: 28px; + height: 28px; + font-size: 1.1rem; + color: #666; +} + +.create-group-modal-content .modal-close:hover { + background: #f5f5f5; + color: #333; + border-color: transparent; + transform: scale(1); +} + +.create-group-modal-content .modal-footer { + padding: 10px 20px; + border-top: 1px solid #f0f0f0; + background: #fafafa; } .create-group-body { - padding: 24px; + padding: 16px 20px; } .create-group-description { - font-size: 0.875rem; - color: var(--text-secondary); - line-height: 1.6; - margin-bottom: 24px; + font-size: 0.8125rem; + color: #666; + line-height: 1.3; + margin-bottom: 12px; + margin-top: 0; + padding: 0; } .create-group-input-wrapper { position: relative; display: flex; align-items: center; + margin-bottom: 0; + width: 100%; } .group-icon-input { position: absolute; - left: 12px; - font-size: 1.2rem; + left: 16px; + top: 50%; + transform: translateY(-50%); + width: auto; + height: auto; + display: flex; + align-items: center; + justify-content: center; + background: none; + border-radius: 0; + font-size: 1rem; pointer-events: none; + z-index: 1; + box-shadow: none; + line-height: 1; } #create-group-name-input { width: 100%; - padding: 12px 16px 12px 48px; - border: 2px solid var(--accent-color); + padding: 8px 12px 8px 40px; + border: 1.5px solid #e0e0e0; border-radius: 8px; - font-size: 0.9375rem; - background: var(--bg-primary); + font-size: 0.875rem; + background: #fafafa; color: var(--text-primary); - transition: all 0.2s ease; + transition: all 0.3s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); + height: 36px; + box-sizing: border-box; + line-height: 1.2; +} + +#create-group-name-input:hover { + border-color: #b0b0b0; + background: #ffffff; } #create-group-name-input:focus { outline: none; - box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); + border-color: #667eea; + background: #ffffff; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), 0 2px 8px rgba(0, 0, 0, 0.08); +} + +#create-group-name-input::placeholder { + color: #999; +} + +.create-group-suggestions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.suggestion-tag { + display: inline-flex; + align-items: center; + padding: 5px 12px; + background: #f5f5f5; + border: 1px solid #e0e0e0; + border-radius: 14px; + font-size: 0.8125rem; + color: #666; + cursor: pointer; + transition: all 0.2s ease; + user-select: none; + height: 26px; + box-sizing: border-box; +} + +.suggestion-tag:hover { + background: #e8e8e8; + border-color: #d0d0d0; + color: #333; + transform: translateY(-1px); +} + +.suggestion-tag:active { + transform: translateY(0); + background: #ddd; } /* 上下文菜单 */ diff --git a/web/static/js/chat.js b/web/static/js/chat.js index ba0710b7..a78c7d63 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -5007,6 +5007,15 @@ function closeCreateGroupModal() { } } +// 选择建议标签 +function selectSuggestion(name) { + const input = document.getElementById('create-group-name-input'); + if (input) { + input.value = name; + input.focus(); + } +} + // 创建分组 async function createGroup(event) { // 阻止事件冒泡 diff --git a/web/static/js/skills.js b/web/static/js/skills.js index ca2fe1d9..79de38b1 100644 --- a/web/static/js/skills.js +++ b/web/static/js/skills.js @@ -165,45 +165,76 @@ function renderSkillsPagination() { } // 计算显示范围 - const start = (currentPage - 1) * pageSize + 1; - const end = Math.min(currentPage * pageSize, total); + const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1; + const end = total === 0 ? 0 : 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); + } } // 改变每页显示数量 @@ -461,7 +492,27 @@ async function saveSkill() { // 删除skill async function deleteSkill(skillName) { - if (!confirm(`确定要删除skill "${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)) { return; } @@ -475,7 +526,14 @@ async function deleteSkill(skillName) { throw new Error(error.error || '删除skill失败'); } - showNotification('skill已删除', 'success'); + 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'); + // 如果当前页没有数据了,回到上一页 const currentPage = skillsPagination.currentPage; const totalAfterDelete = skillsPagination.total - 1; @@ -586,7 +644,7 @@ function renderSkillsMonitor() { - + @@ -604,7 +662,7 @@ function renderSkillsMonitor() { return ` - + diff --git a/web/templates/index.html b/web/templates/index.html index 6dc0395e..0050a08c 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1281,11 +1281,17 @@ version: 1.0.0
×
Skill名称Skill名称 总调用 成功 失败
${escapeHtml(stat.skill_name || '')}${escapeHtml(stat.skill_name || '')} ${totalCalls} ${successCalls} ${failedCalls}