mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d317e6f13f | |||
| 18fa0ad9e7 | |||
| 15a713743f | |||
| 4926335c71 |
@@ -174,7 +174,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
- **Predefined roles** – System includes 12+ predefined security testing roles (Penetration Testing, CTF, Web App Scanning, API Security Testing, Binary Analysis, Cloud Security Audit, etc.) in the `roles/` directory.
|
||||
- **Custom prompts** – Each role can define a `user_prompt` that prepends to user messages, guiding the AI to adopt specialized testing methodologies and focus areas.
|
||||
- **Tool restrictions** – Roles can specify a `tools` list to limit available tools, ensuring focused testing workflows (e.g., CTF role restricts to CTF-specific utilities).
|
||||
- **Skills integration** – Roles can attach security testing skills that are automatically injected into system prompts.
|
||||
- **Skills integration** – Roles can attach security testing skills. Skill names are added to system prompts as hints, and AI agents can access skill content on-demand using the `read_skill` tool.
|
||||
- **Easy role creation** – Create custom roles by adding YAML files to the `roles/` directory. Each role defines `name`, `description`, `user_prompt`, `icon`, `tools`, `skills`, and `enabled` fields.
|
||||
- **Web UI integration** – Select roles from a dropdown in the chat interface. Role selection affects both AI behavior and available tool suggestions.
|
||||
|
||||
@@ -198,7 +198,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
|
||||
### Skills System
|
||||
- **Predefined skills** – System includes 20+ predefined security testing skills (SQL injection, XSS, API security, cloud security, container security, etc.) in the `skills/` directory.
|
||||
- **Automatic injection** – When a role is selected, all skills attached to that role are automatically loaded and injected into the system prompt, providing AI with specialized knowledge and methodologies.
|
||||
- **Skill hints in prompts** – When a role is selected, skill names attached to that role are added to the system prompt as recommendations. Skill content is not automatically injected; AI agents must use the `read_skill` tool to access skill details when needed.
|
||||
- **On-demand access** – AI agents can also access skills on-demand using built-in tools (`list_skills`, `read_skill`), allowing dynamic skill retrieval during task execution.
|
||||
- **Structured format** – Each skill is a directory containing a `SKILL.md` file with detailed testing methods, tool usage, best practices, and examples. Skills support YAML front matter for metadata.
|
||||
- **Custom skills** – Create custom skills by adding directories to the `skills/` directory. Each skill directory should contain a `SKILL.md` file with the skill content.
|
||||
|
||||
+2
-2
@@ -173,7 +173,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
- **预设角色**:系统内置 12+ 个预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试、二进制分析、云安全审计等),位于 `roles/` 目录。
|
||||
- **自定义提示词**:每个角色可定义 `user_prompt`,会在用户消息前自动添加,引导 AI 采用特定的测试方法和关注重点。
|
||||
- **工具限制**:角色可指定 `tools` 列表,限制可用工具,实现聚焦的测试流程(如 CTF 角色限制为 CTF 专用工具)。
|
||||
- **Skills 集成**:角色可附加安全测试技能,选择角色时自动注入到系统提示词中。
|
||||
- **Skills 集成**:角色可附加安全测试技能。技能名称会作为提示添加到系统提示词中,AI 智能体可通过 `read_skill` 工具按需获取技能内容。
|
||||
- **轻松创建角色**:通过在 `roles/` 目录添加 YAML 文件即可创建自定义角色。每个角色定义 `name`、`description`、`user_prompt`、`icon`、`tools`、`skills`、`enabled` 字段。
|
||||
- **Web 界面集成**:在聊天界面通过下拉菜单选择角色。角色选择会影响 AI 行为和可用工具建议。
|
||||
|
||||
@@ -197,7 +197,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
|
||||
### Skills 技能系统
|
||||
- **预设技能**:系统内置 20+ 个预设的安全测试技能(SQL 注入、XSS、API 安全、云安全、容器安全等),位于 `skills/` 目录。
|
||||
- **自动注入**:当选择某个角色时,该角色附加的所有技能会自动加载并注入到系统提示词中,为 AI 提供专业知识和测试方法。
|
||||
- **提示词中的技能提示**:当选择某个角色时,该角色附加的技能名称会作为推荐添加到系统提示词中。技能内容不会自动注入,AI 智能体需要时需使用 `read_skill` 工具获取技能详情。
|
||||
- **按需调用**:AI 智能体也可以通过内置工具(`list_skills`、`read_skill`)按需访问技能,允许在执行任务过程中动态获取相关技能。
|
||||
- **结构化格式**:每个技能是一个目录,包含一个 `SKILL.md` 文件,详细描述测试方法、工具使用、最佳实践和示例。技能支持 YAML front matter 格式用于元数据。
|
||||
- **自定义技能**:通过在 `skills/` 目录添加目录即可创建自定义技能。每个技能目录应包含一个 `SKILL.md` 文件。
|
||||
|
||||
@@ -349,6 +349,18 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
}
|
||||
configHandler.SetVulnerabilityToolRegistrar(vulnerabilityRegistrar)
|
||||
|
||||
// 设置Skills工具注册器(内置工具,必须设置)
|
||||
skillsRegistrar := func() error {
|
||||
// 创建一个适配器,将database.DB适配为SkillStatsStorage接口
|
||||
var skillStatsStorage skills.SkillStatsStorage
|
||||
if db != nil {
|
||||
skillStatsStorage = &skillStatsDBAdapter{db: db}
|
||||
}
|
||||
skills.RegisterSkillsToolWithStorage(mcpServer, skillsManager, skillStatsStorage, log.Logger)
|
||||
return nil
|
||||
}
|
||||
configHandler.SetSkillsToolRegistrar(skillsRegistrar)
|
||||
|
||||
// 设置知识库初始化器(用于动态初始化,需要在 App 创建后设置)
|
||||
configHandler.SetKnowledgeInitializer(func() (*handler.KnowledgeHandler, error) {
|
||||
knowledgeHandler, err := initializeKnowledge(cfg, db, knowledgeDBConn, mcpServer, agentHandler, app, log.Logger)
|
||||
|
||||
@@ -28,6 +28,9 @@ type KnowledgeToolRegistrar func() error
|
||||
// VulnerabilityToolRegistrar 漏洞工具注册器接口
|
||||
type VulnerabilityToolRegistrar func() error
|
||||
|
||||
// SkillsToolRegistrar Skills工具注册器接口
|
||||
type SkillsToolRegistrar func() error
|
||||
|
||||
// RetrieverUpdater 检索器更新接口
|
||||
type RetrieverUpdater interface {
|
||||
UpdateConfig(config *knowledge.RetrievalConfig)
|
||||
@@ -52,6 +55,7 @@ type ConfigHandler struct {
|
||||
externalMCPMgr *mcp.ExternalMCPManager // 外部MCP管理器
|
||||
knowledgeToolRegistrar KnowledgeToolRegistrar // 知识库工具注册器(可选)
|
||||
vulnerabilityToolRegistrar VulnerabilityToolRegistrar // 漏洞工具注册器(可选)
|
||||
skillsToolRegistrar SkillsToolRegistrar // Skills工具注册器(可选)
|
||||
retrieverUpdater RetrieverUpdater // 检索器更新器(可选)
|
||||
knowledgeInitializer KnowledgeInitializer // 知识库初始化器(可选)
|
||||
appUpdater AppUpdater // App更新器(可选)
|
||||
@@ -110,6 +114,13 @@ func (h *ConfigHandler) SetVulnerabilityToolRegistrar(registrar VulnerabilityToo
|
||||
h.vulnerabilityToolRegistrar = registrar
|
||||
}
|
||||
|
||||
// SetSkillsToolRegistrar 设置Skills工具注册器
|
||||
func (h *ConfigHandler) SetSkillsToolRegistrar(registrar SkillsToolRegistrar) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.skillsToolRegistrar = registrar
|
||||
}
|
||||
|
||||
// SetRetrieverUpdater 设置检索器更新器
|
||||
func (h *ConfigHandler) SetRetrieverUpdater(updater RetrieverUpdater) {
|
||||
h.mu.Lock()
|
||||
@@ -869,6 +880,16 @@ func (h *ConfigHandler) ApplyConfig(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 重新注册Skills工具(内置工具,必须注册)
|
||||
if h.skillsToolRegistrar != nil {
|
||||
h.logger.Info("重新注册Skills工具")
|
||||
if err := h.skillsToolRegistrar(); err != nil {
|
||||
h.logger.Error("重新注册Skills工具失败", zap.Error(err))
|
||||
} else {
|
||||
h.logger.Info("Skills工具已重新注册")
|
||||
}
|
||||
}
|
||||
|
||||
// 如果知识库启用,重新注册知识库工具
|
||||
if h.config.Knowledge.Enabled && h.knowledgeToolRegistrar != nil {
|
||||
h.logger.Info("重新注册知识库工具")
|
||||
|
||||
+10
-8
@@ -2,7 +2,7 @@
|
||||
|
||||
## 概述
|
||||
|
||||
Skills系统允许你为角色配置专业知识和技能文档。当角色执行任务时,系统会自动将这些skills的内容注入到系统提示词中,帮助AI更好地理解和执行相关任务。
|
||||
Skills系统允许你为角色配置专业知识和技能文档。当角色执行任务时,系统会将技能名称添加到系统提示词中作为推荐提示,AI智能体可以通过 `read_skill` 工具按需获取技能的详细内容。
|
||||
|
||||
## Skills结构
|
||||
|
||||
@@ -62,16 +62,16 @@ enabled: true
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **加载阶段**:系统启动时,会扫描`skills_dir`目录下的所有skill
|
||||
1. **加载阶段**:系统启动时,会扫描`skills_dir`目录下的所有skill目录
|
||||
2. **执行阶段**:当使用某个角色执行任务时:
|
||||
- 系统会加载该角色配置的所有skills
|
||||
- 将skills内容合并并注入到系统提示词中
|
||||
- AI在执行任务前会阅读这些skills内容
|
||||
3. **按需调用**:即使角色没有配置skills,AI也可以通过以下工具按需调用:
|
||||
- 系统会将角色配置的skill名称添加到系统提示词中作为推荐提示
|
||||
- **注意**:skill的详细内容不会自动注入到系统提示词中
|
||||
- AI智能体需要根据任务需要,主动调用 `read_skill` 工具获取技能的详细内容
|
||||
3. **按需调用**:AI可以通过以下工具访问skills:
|
||||
- `list_skills`: 获取所有可用的skills列表
|
||||
- `read_skill`: 读取指定skill的详细内容
|
||||
|
||||
这样AI可以在执行任务过程中,根据实际需要自主调用相关skills获取专业知识。
|
||||
这样AI可以在执行任务过程中,根据实际需要自主调用相关skills获取专业知识。即使角色没有配置skills,AI也可以通过这些工具按需访问任何可用的skill。
|
||||
|
||||
## 示例Skills
|
||||
|
||||
@@ -106,10 +106,12 @@ EOF
|
||||
|
||||
## 注意事项
|
||||
|
||||
- Skill内容会被注入到系统提示词中,注意控制长度避免超过token限制
|
||||
- **重要**:Skill的详细内容不会自动注入到系统提示词中,只有技能名称会作为提示添加
|
||||
- AI智能体需要通过 `read_skill` 工具主动获取技能内容,这样可以节省token并提高灵活性
|
||||
- Skill内容应该清晰、结构化,便于AI理解
|
||||
- 可以包含代码示例、命令示例等
|
||||
- 建议每个skill专注于一个特定领域或技能
|
||||
- 建议在skill的YAML front matter中提供清晰的 `description`,帮助AI判断是否需要读取该skill
|
||||
|
||||
## 配置
|
||||
|
||||
|
||||
+88
-48
@@ -398,7 +398,7 @@ body {
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 20px 24px;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
@@ -410,14 +410,14 @@ body {
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.page-header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -425,10 +425,18 @@ body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 24px;
|
||||
padding: 16px 20px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 任务管理页面内容区域底部圆角 */
|
||||
#page-tasks .page-content {
|
||||
/* 确保底部左右角都是圆角 */
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 对话页面布局 */
|
||||
.chat-page-layout {
|
||||
display: flex;
|
||||
@@ -3150,18 +3158,20 @@ header {
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
padding: 8px 16px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 40px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
@@ -3263,7 +3273,7 @@ header {
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Skills管理页面分页优化 */
|
||||
@@ -3297,16 +3307,19 @@ header {
|
||||
position: relative;
|
||||
/* 添加四个角的圆角,与上方卡片保持一致 */
|
||||
border-radius: 8px;
|
||||
/* 确保底部左右角都是圆角 */
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination {
|
||||
margin-top: 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 16px 20px;
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-primary);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
/* 确保分页内容与列表内容对齐 */
|
||||
width: 100%;
|
||||
@@ -3315,15 +3328,18 @@ header {
|
||||
border-top-color: rgba(233, 236, 239, 0.6);
|
||||
/* 添加四个角的圆角,与上方卡片保持一致 */
|
||||
border-radius: 8px;
|
||||
/* 确保底部左右角都是圆角 */
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
/* 左侧:信息显示和每页数量选择器 - 更自然的设计 */
|
||||
.pagination-fixed .pagination-info {
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
/* 去掉背景色和边框,更自然 */
|
||||
padding: 0;
|
||||
@@ -3340,25 +3356,27 @@ header {
|
||||
.pagination-fixed .pagination-info .pagination-page-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.875rem;
|
||||
gap: 6px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-info .pagination-page-size select {
|
||||
padding: 6px 10px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 70px;
|
||||
min-width: 60px;
|
||||
font-weight: 500;
|
||||
/* 更柔和的边框 */
|
||||
border-color: rgba(233, 236, 239, 0.8);
|
||||
/* 确保四个角都是圆角 */
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-info .pagination-page-size select:focus {
|
||||
@@ -3382,14 +3400,16 @@ header {
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-controls .btn-secondary {
|
||||
padding: 7px 14px;
|
||||
font-size: 0.875rem;
|
||||
padding: 5px 12px;
|
||||
font-size: 0.8125rem;
|
||||
min-width: auto;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
/* 更柔和的边框 */
|
||||
border-color: rgba(233, 236, 239, 0.8);
|
||||
/* 确保四个角都是圆角 */
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-controls .btn-secondary:hover:not(:disabled) {
|
||||
@@ -3414,11 +3434,18 @@ header {
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-page {
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 0 12px;
|
||||
padding: 5px 10px;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
/* 添加圆角设计,四个角都是圆的 */
|
||||
border-radius: 6px !important;
|
||||
background: var(--bg-secondary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@@ -3598,12 +3625,12 @@ header {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 10px 20px;
|
||||
padding: 7px 16px;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
@@ -3616,12 +3643,12 @@ header {
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 10px 20px;
|
||||
padding: 7px 16px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
@@ -6865,9 +6892,10 @@ header {
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
padding: 16px;
|
||||
background: var(--bg-secondary);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tasks-filters label {
|
||||
@@ -7260,8 +7288,12 @@ header {
|
||||
|
||||
/* 批量任务相关样式 */
|
||||
.batch-queues-section {
|
||||
margin-top: 16px;
|
||||
padding-top: 8px;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
/* 确保底部左右角都是圆角 */
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.batch-queues-header {
|
||||
@@ -7283,6 +7315,7 @@ header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.batch-queue-item {
|
||||
@@ -9218,17 +9251,17 @@ header {
|
||||
|
||||
/* Skills管理页面样式 */
|
||||
.skills-controls {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.skills-stats-bar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.skill-stat-item {
|
||||
@@ -9238,41 +9271,45 @@ header {
|
||||
}
|
||||
|
||||
.skill-stat-label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.skill-stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.skills-filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.skills-filters input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.skills-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.skill-item {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
@@ -9285,7 +9322,7 @@ header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.skill-item-info {
|
||||
@@ -9293,28 +9330,30 @@ header {
|
||||
}
|
||||
|
||||
.skill-item-name {
|
||||
font-size: 1.125rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0 0 4px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.skill-item-desc {
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.skill-item-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -9337,10 +9376,11 @@ header {
|
||||
|
||||
.skill-item-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.skill-meta-item {
|
||||
|
||||
@@ -611,10 +611,6 @@ function renderSkillsMonitor() {
|
||||
<div class="monitor-stat-label">成功率</div>
|
||||
<div class="monitor-stat-value">${successRate}%</div>
|
||||
</div>
|
||||
<div class="monitor-stat-card">
|
||||
<div class="monitor-stat-label">Skills目录</div>
|
||||
<div class="monitor-stat-value" style="font-size: 0.875rem;">${escapeHtml(skillsStats.skillsDir || '-')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+62
-78
@@ -1051,106 +1051,90 @@ function renderBatchQueues() {
|
||||
renderBatchQueuesPagination();
|
||||
}
|
||||
|
||||
// 渲染批量任务队列分页控件
|
||||
// 渲染批量任务队列分页控件(参考Skills管理页面样式)
|
||||
function renderBatchQueuesPagination() {
|
||||
const paginationContainer = document.getElementById('batch-queues-pagination');
|
||||
if (!paginationContainer) return;
|
||||
|
||||
const { currentPage, pageSize, total, totalPages } = batchQueuesState;
|
||||
|
||||
// 如果没有数据,不显示分页控件
|
||||
// 即使只有一页也显示分页信息(参考Skills样式)
|
||||
if (total === 0) {
|
||||
paginationContainer.innerHTML = '';
|
||||
paginationContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保分页控件可见
|
||||
paginationContainer.style.display = '';
|
||||
|
||||
// 即使只有一页,也显示分页信息(总数和每页条数选择器)
|
||||
|
||||
// 计算显示的页码范围
|
||||
let startPage = Math.max(1, currentPage - 2);
|
||||
let endPage = Math.min(totalPages, currentPage + 2);
|
||||
|
||||
// 确保显示5个页码(如果可能)
|
||||
if (endPage - startPage < 4) {
|
||||
if (startPage === 1) {
|
||||
endPage = Math.min(totalPages, startPage + 4);
|
||||
} else if (endPage === totalPages) {
|
||||
startPage = Math.max(1, endPage - 4);
|
||||
}
|
||||
}
|
||||
// 计算显示范围
|
||||
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
|
||||
const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total);
|
||||
|
||||
let paginationHTML = '<div class="pagination">';
|
||||
|
||||
const startItem = (currentPage - 1) * pageSize + 1;
|
||||
const endItem = Math.min(currentPage * pageSize, total);
|
||||
paginationHTML += `<div class="pagination-info">显示 ${startItem}-${endItem} / 共 ${total} 条</div>`;
|
||||
|
||||
// 每页条数选择器
|
||||
// 左侧:显示范围信息和每页数量选择器(参考Skills样式)
|
||||
paginationHTML += `
|
||||
<div class="pagination-page-size">
|
||||
<label for="batch-queues-page-size-pagination">每页:</label>
|
||||
<select id="batch-queues-page-size-pagination" onchange="changeBatchQueuesPageSize()">
|
||||
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
|
||||
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
|
||||
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
|
||||
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
|
||||
</select>
|
||||
<div class="pagination-info">
|
||||
<span>显示 ${start}-${end} / 共 ${total} 条</span>
|
||||
<label class="pagination-page-size">
|
||||
每页显示
|
||||
<select id="batch-queues-page-size-pagination" onchange="changeBatchQueuesPageSize()">
|
||||
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
|
||||
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
|
||||
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
|
||||
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 只有当有多页时才显示页码导航
|
||||
if (totalPages > 1) {
|
||||
paginationHTML += '<div class="pagination-controls">';
|
||||
|
||||
// 上一页按钮
|
||||
if (currentPage > 1) {
|
||||
paginationHTML += `<button class="pagination-btn" onclick="goBatchQueuesPage(${currentPage - 1})" title="上一页">‹</button>`;
|
||||
} else {
|
||||
paginationHTML += '<button class="pagination-btn disabled" disabled>‹</button>';
|
||||
}
|
||||
|
||||
// 第一页
|
||||
if (startPage > 1) {
|
||||
paginationHTML += `<button class="pagination-btn" onclick="goBatchQueuesPage(1)">1</button>`;
|
||||
if (startPage > 2) {
|
||||
paginationHTML += '<span class="pagination-ellipsis">...</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// 页码按钮
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
if (i === currentPage) {
|
||||
paginationHTML += `<button class="pagination-btn active">${i}</button>`;
|
||||
} else {
|
||||
paginationHTML += `<button class="pagination-btn" onclick="goBatchQueuesPage(${i})">${i}</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 最后一页
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
paginationHTML += '<span class="pagination-ellipsis">...</span>';
|
||||
}
|
||||
paginationHTML += `<button class="pagination-btn" onclick="goBatchQueuesPage(${totalPages})">${totalPages}</button>`;
|
||||
}
|
||||
|
||||
// 下一页按钮
|
||||
if (currentPage < totalPages) {
|
||||
paginationHTML += `<button class="pagination-btn" onclick="goBatchQueuesPage(${currentPage + 1})" title="下一页">›</button>`;
|
||||
} else {
|
||||
paginationHTML += '<button class="pagination-btn disabled" disabled>›</button>';
|
||||
}
|
||||
|
||||
paginationHTML += '</div>';
|
||||
}
|
||||
// 右侧:分页按钮(参考Skills样式:首页、上一页、第X/Y页、下一页、末页)
|
||||
paginationHTML += `
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
|
||||
<span class="pagination-page">第 ${currentPage} / ${totalPages || 1} 页</span>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
paginationHTML += '</div>';
|
||||
|
||||
paginationContainer.innerHTML = paginationHTML;
|
||||
|
||||
// 确保分页组件与列表内容区域对齐(不包括滚动条)
|
||||
function alignPaginationWidth() {
|
||||
const batchQueuesList = document.getElementById('batch-queues-list');
|
||||
if (batchQueuesList && paginationContainer) {
|
||||
// 获取列表的实际内容宽度(不包括滚动条)
|
||||
const listClientWidth = batchQueuesList.clientWidth; // 可视区域宽度(不包括滚动条)
|
||||
const listScrollHeight = batchQueuesList.scrollHeight; // 内容总高度
|
||||
const listClientHeight = batchQueuesList.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 batchQueuesList = document.getElementById('batch-queues-list');
|
||||
if (batchQueuesList) {
|
||||
resizeObserver.observe(batchQueuesList);
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到指定页面
|
||||
|
||||
@@ -665,7 +665,7 @@
|
||||
</div>
|
||||
<div id="batch-queues-list" class="batch-queues-list"></div>
|
||||
<!-- 分页控件 -->
|
||||
<div id="batch-queues-pagination"></div>
|
||||
<div id="batch-queues-pagination" class="pagination-container pagination-fixed"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user