diff --git a/internal/database/batch_task.go b/internal/database/batch_task.go index 1847c73d..22a79eef 100644 --- a/internal/database/batch_task.go +++ b/internal/database/batch_task.go @@ -11,6 +11,7 @@ import ( // BatchTaskQueueRow 批量任务队列数据库行 type BatchTaskQueueRow struct { ID string + Title sql.NullString Status string CreatedAt time.Time StartedAt sql.NullTime @@ -32,7 +33,7 @@ type BatchTaskRow struct { } // CreateBatchQueue 创建批量任务队列 -func (db *DB) CreateBatchQueue(queueID string, tasks []map[string]interface{}) error { +func (db *DB) CreateBatchQueue(queueID string, title string, tasks []map[string]interface{}) error { tx, err := db.Begin() if err != nil { return fmt.Errorf("开始事务失败: %w", err) @@ -41,8 +42,8 @@ func (db *DB) CreateBatchQueue(queueID string, tasks []map[string]interface{}) e now := time.Now() _, err = tx.Exec( - "INSERT INTO batch_task_queues (id, status, created_at, current_index) VALUES (?, ?, ?, ?)", - queueID, "pending", now, 0, + "INSERT INTO batch_task_queues (id, title, status, created_at, current_index) VALUES (?, ?, ?, ?, ?)", + queueID, title, "pending", now, 0, ) if err != nil { return fmt.Errorf("创建批量任务队列失败: %w", err) @@ -76,9 +77,9 @@ func (db *DB) GetBatchQueue(queueID string) (*BatchTaskQueueRow, error) { var row BatchTaskQueueRow var createdAt string err := db.QueryRow( - "SELECT id, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE id = ?", + "SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE id = ?", queueID, - ).Scan(&row.ID, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex) + ).Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex) if err == sql.ErrNoRows { return nil, nil } @@ -102,7 +103,7 @@ func (db *DB) GetBatchQueue(queueID string) (*BatchTaskQueueRow, error) { // GetAllBatchQueues 获取所有批量任务队列 func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) { rows, err := db.Query( - "SELECT id, status, created_at, started_at, completed_at, current_index FROM batch_task_queues ORDER BY created_at DESC", + "SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues ORDER BY created_at DESC", ) if err != nil { return nil, fmt.Errorf("查询批量任务队列列表失败: %w", err) @@ -113,7 +114,7 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) { for rows.Next() { var row BatchTaskQueueRow var createdAt string - if err := rows.Scan(&row.ID, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil { + if err := rows.Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil { return nil, fmt.Errorf("扫描批量任务队列失败: %w", err) } parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt) @@ -133,7 +134,7 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) { // ListBatchQueues 列出批量任务队列(支持筛选和分页) func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*BatchTaskQueueRow, error) { - query := "SELECT id, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE 1=1" + query := "SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE 1=1" args := []interface{}{} // 状态筛选 @@ -142,10 +143,10 @@ func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*Bat args = append(args, status) } - // 关键字搜索(搜索队列ID) + // 关键字搜索(搜索队列ID和标题) if keyword != "" { - query += " AND id LIKE ?" - args = append(args, "%"+keyword+"%") + query += " AND (id LIKE ? OR title LIKE ?)" + args = append(args, "%"+keyword+"%", "%"+keyword+"%") } query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" @@ -161,7 +162,7 @@ func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*Bat for rows.Next() { var row BatchTaskQueueRow var createdAt string - if err := rows.Scan(&row.ID, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil { + if err := rows.Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil { return nil, fmt.Errorf("扫描批量任务队列失败: %w", err) } parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt) @@ -190,10 +191,10 @@ func (db *DB) CountBatchQueues(status, keyword string) (int, error) { args = append(args, status) } - // 关键字搜索 + // 关键字搜索(搜索队列ID和标题) if keyword != "" { - query += " AND id LIKE ?" - args = append(args, "%"+keyword+"%") + query += " AND (id LIKE ? OR title LIKE ?)" + args = append(args, "%"+keyword+"%", "%"+keyword+"%") } var count int diff --git a/internal/database/database.go b/internal/database/database.go index 4b962e3b..5b3cd137 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -193,6 +193,7 @@ func (db *DB) initTables() error { createBatchTaskQueuesTable := ` CREATE TABLE IF NOT EXISTS batch_task_queues ( id TEXT PRIMARY KEY, + title TEXT, status TEXT NOT NULL, created_at DATETIME NOT NULL, started_at DATETIME, @@ -240,6 +241,7 @@ func (db *DB) initTables() error { CREATE INDEX IF NOT EXISTS idx_vulnerabilities_created_at ON vulnerabilities(created_at); CREATE INDEX IF NOT EXISTS idx_batch_tasks_queue_id ON batch_tasks(queue_id); CREATE INDEX IF NOT EXISTS idx_batch_task_queues_created_at ON batch_task_queues(created_at); + CREATE INDEX IF NOT EXISTS idx_batch_task_queues_title ON batch_task_queues(title); ` if _, err := db.Exec(createConversationsTable); err != nil { @@ -310,6 +312,11 @@ func (db *DB) initTables() error { // 不返回错误,允许继续运行 } + if err := db.migrateBatchTaskQueuesTable(); err != nil { + db.logger.Warn("迁移batch_task_queues表失败", zap.Error(err)) + // 不返回错误,允许继续运行 + } + if _, err := db.Exec(createIndexes); err != nil { return fmt.Errorf("创建索引失败: %w", err) } @@ -426,6 +433,30 @@ func (db *DB) migrateConversationGroupMappingsTable() error { return nil } +// migrateBatchTaskQueuesTable 迁移batch_task_queues表,添加title字段 +func (db *DB) migrateBatchTaskQueuesTable() error { + // 检查title字段是否存在 + var count int + err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('batch_task_queues') WHERE name='title'").Scan(&count) + if err != nil { + // 如果查询失败,尝试添加字段 + if _, addErr := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN title TEXT"); addErr != nil { + // 如果字段已存在,忽略错误 + errMsg := strings.ToLower(addErr.Error()) + if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") { + db.logger.Warn("添加title字段失败", zap.Error(addErr)) + } + } + } else if count == 0 { + // 字段不存在,添加它 + if _, err := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN title TEXT"); err != nil { + db.logger.Warn("添加title字段失败", zap.Error(err)) + } + } + + return nil +} + // NewKnowledgeDB 创建知识库数据库连接(只包含知识库相关的表) func NewKnowledgeDB(dbPath string, logger *zap.Logger) (*DB, error) { sqlDB, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_foreign_keys=1") diff --git a/internal/handler/agent.go b/internal/handler/agent.go index 738d4618..015cc5ab 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -759,6 +759,7 @@ func (h *AgentHandler) ListCompletedTasks(c *gin.Context) { // BatchTaskRequest 批量任务请求 type BatchTaskRequest struct { + Title string `json:"title"` // 任务标题(可选) Tasks []string `json:"tasks" binding:"required"` // 任务列表,每行一个任务 } @@ -788,7 +789,7 @@ func (h *AgentHandler) CreateBatchQueue(c *gin.Context) { return } - queue := h.batchTaskManager.CreateBatchQueue(validTasks) + queue := h.batchTaskManager.CreateBatchQueue(req.Title, validTasks) c.JSON(http.StatusOK, gin.H{ "queueId": queue.ID, "queue": queue, diff --git a/internal/handler/batch_task_manager.go b/internal/handler/batch_task_manager.go index 0b5d8bbd..24ada0c6 100644 --- a/internal/handler/batch_task_manager.go +++ b/internal/handler/batch_task_manager.go @@ -28,6 +28,7 @@ type BatchTask struct { // BatchTaskQueue 批量任务队列 type BatchTaskQueue struct { ID string `json:"id"` + Title string `json:"title,omitempty"` Tasks []*BatchTask `json:"tasks"` Status string `json:"status"` // pending, running, paused, completed, cancelled CreatedAt time.Time `json:"createdAt"` @@ -61,13 +62,14 @@ func (m *BatchTaskManager) SetDB(db *database.DB) { } // CreateBatchQueue 创建批量任务队列 -func (m *BatchTaskManager) CreateBatchQueue(tasks []string) *BatchTaskQueue { +func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *BatchTaskQueue { m.mu.Lock() defer m.mu.Unlock() queueID := time.Now().Format("20060102150405") + "-" + generateShortID() queue := &BatchTaskQueue{ ID: queueID, + Title: title, Tasks: make([]*BatchTask, 0, len(tasks)), Status: "pending", CreatedAt: time.Now(), @@ -96,7 +98,7 @@ func (m *BatchTaskManager) CreateBatchQueue(tasks []string) *BatchTaskQueue { // 保存到数据库 if m.db != nil { - if err := m.db.CreateBatchQueue(queueID, dbTasks); err != nil { + if err := m.db.CreateBatchQueue(queueID, title, dbTasks); err != nil { // 如果数据库保存失败,记录错误但继续(使用内存缓存) // 这里可以添加日志记录 } @@ -153,6 +155,9 @@ func (m *BatchTaskManager) loadQueueFromDB(queueID string) *BatchTaskQueue { Tasks: make([]*BatchTask, 0, len(taskRows)), } + if queueRow.Title.Valid { + queue.Title = queueRow.Title.String + } if queueRow.StartedAt.Valid { queue.StartedAt = &queueRow.StartedAt.Time } @@ -271,11 +276,12 @@ func (m *BatchTaskManager) ListQueues(limit, offset int, status, keyword string) if status != "" && status != "all" && queue.Status != status { continue } - // 关键字搜索 + // 关键字搜索(搜索队列ID和标题) if keyword != "" { keywordLower := strings.ToLower(keyword) queueIDLower := strings.ToLower(queue.ID) - if !strings.Contains(queueIDLower, keywordLower) { + queueTitleLower := strings.ToLower(queue.Title) + if !strings.Contains(queueIDLower, keywordLower) && !strings.Contains(queueTitleLower, keywordLower) { // 也可以搜索创建时间 createdAtStr := queue.CreatedAt.Format("2006-01-02 15:04:05") if !strings.Contains(createdAtStr, keyword) { @@ -342,6 +348,9 @@ func (m *BatchTaskManager) LoadFromDB() error { Tasks: make([]*BatchTask, 0, len(taskRows)), } + if queueRow.Title.Valid { + queue.Title = queueRow.Title.String + } if queueRow.StartedAt.Valid { queue.StartedAt = &queueRow.StartedAt.Time } diff --git a/web/static/css/style.css b/web/static/css/style.css index 7adb409e..5ef913c2 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -6615,7 +6615,6 @@ header { align-items: center; margin-bottom: 16px; padding-bottom: 12px; - border-bottom: 1px solid var(--border-color); } .batch-queues-header h3 { diff --git a/web/static/js/tasks.js b/web/static/js/tasks.js index e12ae874..cd01fe74 100644 --- a/web/static/js/tasks.js +++ b/web/static/js/tasks.js @@ -720,8 +720,12 @@ const batchQueuesState = { function showBatchImportModal() { const modal = document.getElementById('batch-import-modal'); const input = document.getElementById('batch-tasks-input'); + const titleInput = document.getElementById('batch-queue-title'); if (modal && input) { input.value = ''; + if (titleInput) { + titleInput.value = ''; + } updateBatchImportStats(''); modal.style.display = 'block'; input.focus(); @@ -765,6 +769,7 @@ document.addEventListener('DOMContentLoaded', function() { // 创建批量任务队列 async function createBatchQueue() { const input = document.getElementById('batch-tasks-input'); + const titleInput = document.getElementById('batch-queue-title'); if (!input) return; const text = input.value.trim(); @@ -780,13 +785,16 @@ async function createBatchQueue() { return; } + // 获取标题(可选) + const title = titleInput ? titleInput.value.trim() : ''; + try { const response = await apiFetch('/api/batch-tasks', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ tasks }), + body: JSON.stringify({ title, tasks }), }); if (!response.ok) { @@ -918,10 +926,13 @@ function renderBatchQueues() { // 允许删除待执行、已完成或已取消状态的队列 const canDelete = queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled'; + const titleDisplay = queue.title ? `${escapeHtml(queue.title)}` : ''; + return `
+ ${titleDisplay} ${status.text} 队列ID: ${escapeHtml(queue.id)} 创建时间: ${new Date(queue.createdAt).toLocaleString('zh-CN')} @@ -1100,7 +1111,7 @@ async function showBatchQueueDetail(queueId) { batchQueuesState.currentQueueId = queueId; if (title) { - title.textContent = '批量任务队列'; + title.textContent = queue.title ? `批量任务队列 - ${escapeHtml(queue.title)}` : '批量任务队列'; } // 更新按钮显示 @@ -1146,6 +1157,10 @@ async function showBatchQueueDetail(queueId) { content.innerHTML = `
+ ${queue.title ? `
+ 任务标题 + ${escapeHtml(queue.title)} +
` : ''}
队列ID ${escapeHtml(queue.id)} diff --git a/web/templates/index.html b/web/templates/index.html index 5d914603..af33a87d 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -568,9 +568,6 @@