Add files via upload

This commit is contained in:
公明
2026-01-02 02:16:44 +08:00
committed by GitHub
parent 88e710d7e9
commit 044480a427
8 changed files with 786 additions and 196 deletions
+1 -1
View File
@@ -430,7 +430,7 @@ func setupRoutes(
protected.GET("/batch-tasks", agentHandler.ListBatchQueues)
protected.GET("/batch-tasks/:queueId", agentHandler.GetBatchQueue)
protected.POST("/batch-tasks/:queueId/start", agentHandler.StartBatchQueue)
protected.POST("/batch-tasks/:queueId/cancel", agentHandler.CancelBatchQueue)
protected.POST("/batch-tasks/:queueId/pause", agentHandler.PauseBatchQueue)
protected.DELETE("/batch-tasks/:queueId", agentHandler.DeleteBatchQueue)
protected.PUT("/batch-tasks/:queueId/tasks/:taskId", agentHandler.UpdateBatchTask)
protected.POST("/batch-tasks/:queueId/tasks", agentHandler.AddBatchTask)
+74
View File
@@ -131,6 +131,80 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
return queues, nil
}
// 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"
args := []interface{}{}
// 状态筛选
if status != "" && status != "all" {
query += " AND status = ?"
args = append(args, status)
}
// 关键字搜索(搜索队列ID
if keyword != "" {
query += " AND id LIKE ?"
args = append(args, "%"+keyword+"%")
}
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := db.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("查询批量任务队列列表失败: %w", err)
}
defer rows.Close()
var queues []*BatchTaskQueueRow
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 {
return nil, fmt.Errorf("扫描批量任务队列失败: %w", err)
}
parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt)
if parseErr != nil {
parsedTime, parseErr = time.Parse(time.RFC3339, createdAt)
if parseErr != nil {
db.logger.Warn("解析创建时间失败", zap.String("createdAt", createdAt), zap.Error(parseErr))
parsedTime = time.Now()
}
}
row.CreatedAt = parsedTime
queues = append(queues, &row)
}
return queues, nil
}
// CountBatchQueues 统计批量任务队列总数(支持筛选条件)
func (db *DB) CountBatchQueues(status, keyword string) (int, error) {
query := "SELECT COUNT(*) FROM batch_task_queues WHERE 1=1"
args := []interface{}{}
// 状态筛选
if status != "" && status != "all" {
query += " AND status = ?"
args = append(args, status)
}
// 关键字搜索
if keyword != "" {
query += " AND id LIKE ?"
args = append(args, "%"+keyword+"%")
}
var count int
err := db.QueryRow(query, args...).Scan(&count)
if err != nil {
return 0, fmt.Errorf("统计批量任务队列总数失败: %w", err)
}
return count, nil
}
// GetBatchTasks 获取批量任务队列的所有任务
func (db *DB) GetBatchTasks(queueID string) ([]*BatchTaskRow, error) {
rows, err := db.Query(
+296 -151
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"unicode/utf8"
@@ -214,141 +215,17 @@ type StreamEvent struct {
Data interface{} `json:"data,omitempty"`
}
// AgentLoopStream 处理Agent Loop流式请求
func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
// 对于流式请求,也发送SSE格式的错误
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
event := StreamEvent{
Type: "error",
Message: "请求参数错误: " + err.Error(),
}
eventJSON, _ := json.Marshal(event)
fmt.Fprintf(c.Writer, "data: %s\n\n", eventJSON)
c.Writer.Flush()
return
}
h.logger.Info("收到Agent Loop流式请求",
zap.String("message", req.Message),
zap.String("conversationId", req.ConversationID),
)
// 设置SSE响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no") // 禁用nginx缓冲
// 发送初始事件
// 用于跟踪客户端是否已断开连接
clientDisconnected := false
sendEvent := func(eventType, message string, data interface{}) {
// 如果客户端已断开,不再发送事件
if clientDisconnected {
return
}
// 检查请求上下文是否被取消(客户端断开)
select {
case <-c.Request.Context().Done():
clientDisconnected = true
return
default:
}
event := StreamEvent{
Type: eventType,
Message: message,
Data: data,
}
eventJSON, _ := json.Marshal(event)
// 尝试写入事件,如果失败则标记客户端断开
if _, err := fmt.Fprintf(c.Writer, "data: %s\n\n", eventJSON); err != nil {
clientDisconnected = true
h.logger.Debug("客户端断开连接,停止发送SSE事件", zap.Error(err))
return
}
// 刷新响应,如果失败则标记客户端断开
if flusher, ok := c.Writer.(http.Flusher); ok {
flusher.Flush()
} else {
c.Writer.Flush()
}
}
// 如果没有对话ID,创建新对话
conversationID := req.ConversationID
if conversationID == "" {
title := safeTruncateString(req.Message, 50)
conv, err := h.db.CreateConversation(title)
if err != nil {
h.logger.Error("创建对话失败", zap.Error(err))
sendEvent("error", "创建对话失败: "+err.Error(), nil)
return
}
conversationID = conv.ID
}
sendEvent("conversation", "会话已创建", map[string]interface{}{
"conversationId": conversationID,
})
// 优先尝试从保存的ReAct数据恢复历史上下文
agentHistoryMessages, err := h.loadHistoryFromReActData(conversationID)
if err != nil {
h.logger.Warn("从ReAct数据加载历史消息失败,使用消息表", zap.Error(err))
// 回退到使用数据库消息表
historyMessages, err := h.db.GetMessages(conversationID)
if err != nil {
h.logger.Warn("获取历史消息失败", zap.Error(err))
agentHistoryMessages = []agent.ChatMessage{}
} else {
// 将数据库消息转换为Agent消息格式
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
for _, msg := range historyMessages {
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
Role: msg.Role,
Content: msg.Content,
})
}
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
}
} else {
h.logger.Info("从ReAct数据恢复历史上下文", zap.Int("count", len(agentHistoryMessages)))
}
// 保存用户消息
_, err = h.db.AddMessage(conversationID, "user", req.Message, nil)
if err != nil {
h.logger.Error("保存用户消息失败", zap.Error(err))
}
// 预先创建助手消息,以便关联过程详情
assistantMsg, err := h.db.AddMessage(conversationID, "assistant", "处理中...", nil)
if err != nil {
h.logger.Error("创建助手消息失败", zap.Error(err))
// 如果创建失败,继续执行但不保存过程详情
assistantMsg = nil
}
// 创建进度回调函数,同时保存到数据库
var assistantMessageID string
if assistantMsg != nil {
assistantMessageID = assistantMsg.ID
}
// createProgressCallback 创建进度回调函数,用于保存processDetails
// sendEventFunc: 可选的流式事件发送函数,如果为nil则不发送流式事件
func (h *AgentHandler) createProgressCallback(conversationID, assistantMessageID string, sendEventFunc func(eventType, message string, data interface{})) agent.ProgressCallback {
// 用于保存tool_call事件中的参数,以便在tool_result时使用
toolCallCache := make(map[string]map[string]interface{}) // toolCallId -> arguments
progressCallback := func(eventType, message string, data interface{}) {
sendEvent(eventType, message, data)
return func(eventType, message string, data interface{}) {
// 如果提供了sendEventFunc,发送流式事件
if sendEventFunc != nil {
sendEventFunc(eventType, message, data)
}
// 保存tool_call事件中的参数
if eventType == "tool_call" {
@@ -481,6 +358,140 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
}
}
}
}
// AgentLoopStream 处理Agent Loop流式请求
func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
// 对于流式请求,也发送SSE格式的错误
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
event := StreamEvent{
Type: "error",
Message: "请求参数错误: " + err.Error(),
}
eventJSON, _ := json.Marshal(event)
fmt.Fprintf(c.Writer, "data: %s\n\n", eventJSON)
c.Writer.Flush()
return
}
h.logger.Info("收到Agent Loop流式请求",
zap.String("message", req.Message),
zap.String("conversationId", req.ConversationID),
)
// 设置SSE响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no") // 禁用nginx缓冲
// 发送初始事件
// 用于跟踪客户端是否已断开连接
clientDisconnected := false
sendEvent := func(eventType, message string, data interface{}) {
// 如果客户端已断开,不再发送事件
if clientDisconnected {
return
}
// 检查请求上下文是否被取消(客户端断开)
select {
case <-c.Request.Context().Done():
clientDisconnected = true
return
default:
}
event := StreamEvent{
Type: eventType,
Message: message,
Data: data,
}
eventJSON, _ := json.Marshal(event)
// 尝试写入事件,如果失败则标记客户端断开
if _, err := fmt.Fprintf(c.Writer, "data: %s\n\n", eventJSON); err != nil {
clientDisconnected = true
h.logger.Debug("客户端断开连接,停止发送SSE事件", zap.Error(err))
return
}
// 刷新响应,如果失败则标记客户端断开
if flusher, ok := c.Writer.(http.Flusher); ok {
flusher.Flush()
} else {
c.Writer.Flush()
}
}
// 如果没有对话ID,创建新对话
conversationID := req.ConversationID
if conversationID == "" {
title := safeTruncateString(req.Message, 50)
conv, err := h.db.CreateConversation(title)
if err != nil {
h.logger.Error("创建对话失败", zap.Error(err))
sendEvent("error", "创建对话失败: "+err.Error(), nil)
return
}
conversationID = conv.ID
}
sendEvent("conversation", "会话已创建", map[string]interface{}{
"conversationId": conversationID,
})
// 优先尝试从保存的ReAct数据恢复历史上下文
agentHistoryMessages, err := h.loadHistoryFromReActData(conversationID)
if err != nil {
h.logger.Warn("从ReAct数据加载历史消息失败,使用消息表", zap.Error(err))
// 回退到使用数据库消息表
historyMessages, err := h.db.GetMessages(conversationID)
if err != nil {
h.logger.Warn("获取历史消息失败", zap.Error(err))
agentHistoryMessages = []agent.ChatMessage{}
} else {
// 将数据库消息转换为Agent消息格式
agentHistoryMessages = make([]agent.ChatMessage, 0, len(historyMessages))
for _, msg := range historyMessages {
agentHistoryMessages = append(agentHistoryMessages, agent.ChatMessage{
Role: msg.Role,
Content: msg.Content,
})
}
h.logger.Info("从消息表加载历史消息", zap.Int("count", len(agentHistoryMessages)))
}
} else {
h.logger.Info("从ReAct数据恢复历史上下文", zap.Int("count", len(agentHistoryMessages)))
}
// 保存用户消息
_, err = h.db.AddMessage(conversationID, "user", req.Message, nil)
if err != nil {
h.logger.Error("保存用户消息失败", zap.Error(err))
}
// 预先创建助手消息,以便关联过程详情
assistantMsg, err := h.db.AddMessage(conversationID, "assistant", "处理中...", nil)
if err != nil {
h.logger.Error("创建助手消息失败", zap.Error(err))
// 如果创建失败,继续执行但不保存过程详情
assistantMsg = nil
}
// 创建进度回调函数,同时保存到数据库
var assistantMessageID string
if assistantMsg != nil {
assistantMessageID = assistantMsg.ID
}
// 创建进度回调函数,复用统一逻辑
progressCallback := h.createProgressCallback(conversationID, assistantMessageID, sendEvent)
// 创建一个独立的上下文用于任务执行,不随HTTP请求取消
// 这样即使客户端断开连接(如刷新页面),任务也能继续执行
@@ -795,10 +806,76 @@ func (h *AgentHandler) GetBatchQueue(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"queue": queue})
}
// ListBatchQueues 列出所有批量任务队列
// ListBatchQueuesResponse 批量任务队列列表响应
type ListBatchQueuesResponse struct {
Queues []*BatchTaskQueue `json:"queues"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// ListBatchQueues 列出所有批量任务队列(支持筛选和分页)
func (h *AgentHandler) ListBatchQueues(c *gin.Context) {
queues := h.batchTaskManager.GetAllQueues()
c.JSON(http.StatusOK, gin.H{"queues": queues})
limitStr := c.DefaultQuery("limit", "10")
offsetStr := c.DefaultQuery("offset", "0")
pageStr := c.Query("page")
status := c.Query("status")
keyword := c.Query("keyword")
limit, _ := strconv.Atoi(limitStr)
offset, _ := strconv.Atoi(offsetStr)
page := 1
// 如果提供了page参数,优先使用page计算offset
if pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
offset = (page - 1) * limit
}
}
// 限制pageSize范围
if limit <= 0 || limit > 100 {
limit = 10
}
if offset < 0 {
offset = 0
}
// 默认status为"all"
if status == "" {
status = "all"
}
// 获取队列列表和总数
queues, total, err := h.batchTaskManager.ListQueues(limit, offset, status, keyword)
if err != nil {
h.logger.Error("获取批量任务队列列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 计算总页数
totalPages := (total + limit - 1) / limit
if totalPages == 0 {
totalPages = 1
}
// 如果使用offset计算page,需要重新计算
if pageStr == "" {
page = (offset / limit) + 1
}
response := ListBatchQueuesResponse{
Queues: queues,
Total: total,
Page: page,
PageSize: limit,
TotalPages: totalPages,
}
c.JSON(http.StatusOK, response)
}
// StartBatchQueue 开始执行批量任务队列
@@ -822,15 +899,15 @@ func (h *AgentHandler) StartBatchQueue(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "批量任务已开始执行", "queueId": queueID})
}
// CancelBatchQueue 取消批量任务队列
func (h *AgentHandler) CancelBatchQueue(c *gin.Context) {
// PauseBatchQueue 暂停批量任务队列
func (h *AgentHandler) PauseBatchQueue(c *gin.Context) {
queueID := c.Param("queueId")
success := h.batchTaskManager.CancelQueue(queueID)
success := h.batchTaskManager.PauseQueue(queueID)
if !success {
c.JSON(http.StatusNotFound, gin.H{"error": "队列不存在或无法取消"})
c.JSON(http.StatusNotFound, gin.H{"error": "队列不存在或无法暂停"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "批量任务已取消"})
c.JSON(http.StatusOK, gin.H{"message": "批量任务已暂停"})
}
// DeleteBatchQueue 删除批量任务队列
@@ -936,7 +1013,7 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
for {
// 检查队列状态
queue, exists := h.batchTaskManager.GetBatchQueue(queueID)
if !exists || queue.Status == "cancelled" || queue.Status == "completed" {
if !exists || queue.Status == "cancelled" || queue.Status == "completed" || queue.Status == "paused" {
break
}
@@ -973,13 +1050,28 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
h.logger.Error("保存用户消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
}
// 预先创建助手消息,以便关联过程详情
assistantMsg, err := h.db.AddMessage(conversationID, "assistant", "处理中...", nil)
if err != nil {
h.logger.Error("创建助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
// 如果创建失败,继续执行但不保存过程详情
assistantMsg = nil
}
// 创建进度回调函数,复用统一逻辑(批量任务不需要流式事件,所以传入nil)
var assistantMessageID string
if assistantMsg != nil {
assistantMessageID = assistantMsg.ID
}
progressCallback := h.createProgressCallback(conversationID, assistantMessageID, nil)
// 执行任务
h.logger.Info("执行批量任务", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("message", task.Message), zap.String("conversationId", conversationID))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
// 存储取消函数,以便在取消队列时能够取消当前任务
h.batchTaskManager.SetTaskCancel(queueID, cancel)
result, err := h.agent.AgentLoopWithConversationID(ctx, task.Message, []agent.ChatMessage{}, conversationID)
result, err := h.agent.AgentLoopWithProgress(ctx, task.Message, []agent.ChatMessage{}, conversationID, progressCallback)
// 任务执行完成,清理取消函数
h.batchTaskManager.SetTaskCancel(queueID, nil)
cancel()
@@ -1002,9 +1094,25 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
if result != nil && result.Response != "" && (strings.Contains(result.Response, "任务已被取消") || strings.Contains(result.Response, "任务执行中断")) {
cancelMsg = result.Response
}
_, errMsg := h.db.AddMessage(conversationID, "assistant", cancelMsg, nil)
if errMsg != nil {
h.logger.Warn("保存取消消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(errMsg))
// 更新助手消息内容
if assistantMessageID != "" {
if _, updateErr := h.db.Exec(
"UPDATE messages SET content = ? WHERE id = ?",
cancelMsg,
assistantMessageID,
); updateErr != nil {
h.logger.Warn("更新取消后的助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(updateErr))
}
// 保存取消详情到数据库
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, "cancelled", cancelMsg, nil); err != nil {
h.logger.Warn("保存取消详情失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(err))
}
} else {
// 如果没有预先创建的助手消息,创建一个新的
_, errMsg := h.db.AddMessage(conversationID, "assistant", cancelMsg, nil)
if errMsg != nil {
h.logger.Warn("保存取消消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(errMsg))
}
}
// 保存ReAct数据(如果存在)
if result != nil && (result.LastReActInput != "" || result.LastReActOutput != "") {
@@ -1015,15 +1123,52 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
h.batchTaskManager.UpdateTaskStatusWithConversationID(queueID, task.ID, "cancelled", cancelMsg, "", conversationID)
} else {
h.logger.Error("批量任务执行失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
errorMsg := "执行失败: " + err.Error()
// 更新助手消息内容
if assistantMessageID != "" {
if _, updateErr := h.db.Exec(
"UPDATE messages SET content = ? WHERE id = ?",
errorMsg,
assistantMessageID,
); updateErr != nil {
h.logger.Warn("更新失败后的助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(updateErr))
}
// 保存错误详情到数据库
if err := h.db.AddProcessDetail(assistantMessageID, conversationID, "error", errorMsg, nil); err != nil {
h.logger.Warn("保存错误详情失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(err))
}
}
h.batchTaskManager.UpdateTaskStatus(queueID, task.ID, "failed", "", err.Error())
}
} else {
h.logger.Info("批量任务执行成功", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID))
// 保存助手回复
_, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs)
if err != nil {
h.logger.Error("保存助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
// 更新助手消息内容
if assistantMessageID != "" {
mcpIDsJSON := ""
if len(result.MCPExecutionIDs) > 0 {
jsonData, _ := json.Marshal(result.MCPExecutionIDs)
mcpIDsJSON = string(jsonData)
}
if _, updateErr := h.db.Exec(
"UPDATE messages SET content = ?, mcp_execution_ids = ? WHERE id = ?",
result.Response,
mcpIDsJSON,
assistantMessageID,
); updateErr != nil {
h.logger.Warn("更新助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.Error(updateErr))
// 如果更新失败,尝试创建新消息
_, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs)
if err != nil {
h.logger.Error("保存助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
}
}
} else {
// 如果没有预先创建的助手消息,创建一个新的
_, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs)
if err != nil {
h.logger.Error("保存助手消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
}
}
// 保存ReAct数据
@@ -1042,9 +1187,9 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
// 移动到下一个任务
h.batchTaskManager.MoveToNextTask(queueID)
// 检查是否被取消
// 检查是否被取消或暂停
queue, _ = h.batchTaskManager.GetBatchQueue(queueID)
if queue.Status == "cancelled" {
if queue.Status == "cancelled" || queue.Status == "paused" {
break
}
}
+132 -1
View File
@@ -5,6 +5,8 @@ import (
"crypto/rand"
"encoding/hex"
"fmt"
"sort"
"strings"
"sync"
"time"
@@ -214,6 +216,100 @@ func (m *BatchTaskManager) GetAllQueues() []*BatchTaskQueue {
return result
}
// ListQueues 列出队列(支持筛选和分页)
func (m *BatchTaskManager) ListQueues(limit, offset int, status, keyword string) ([]*BatchTaskQueue, int, error) {
var queues []*BatchTaskQueue
var total int
// 如果数据库可用,从数据库查询
if m.db != nil {
// 获取总数
count, err := m.db.CountBatchQueues(status, keyword)
if err != nil {
return nil, 0, fmt.Errorf("统计队列总数失败: %w", err)
}
total = count
// 获取队列列表(只获取ID
queueRows, err := m.db.ListBatchQueues(limit, offset, status, keyword)
if err != nil {
return nil, 0, fmt.Errorf("查询队列列表失败: %w", err)
}
// 加载完整的队列信息(从内存或数据库)
m.mu.Lock()
for _, queueRow := range queueRows {
var queue *BatchTaskQueue
// 先从内存查找
if cached, exists := m.queues[queueRow.ID]; exists {
queue = cached
} else {
// 从数据库加载
queue = m.loadQueueFromDB(queueRow.ID)
if queue != nil {
m.queues[queueRow.ID] = queue
}
}
if queue != nil {
queues = append(queues, queue)
}
}
m.mu.Unlock()
} else {
// 没有数据库,从内存中筛选和分页
m.mu.RLock()
allQueues := make([]*BatchTaskQueue, 0, len(m.queues))
for _, queue := range m.queues {
allQueues = append(allQueues, queue)
}
m.mu.RUnlock()
// 筛选
filtered := make([]*BatchTaskQueue, 0)
for _, queue := range allQueues {
// 状态筛选
if status != "" && status != "all" && queue.Status != status {
continue
}
// 关键字搜索
if keyword != "" {
keywordLower := strings.ToLower(keyword)
queueIDLower := strings.ToLower(queue.ID)
if !strings.Contains(queueIDLower, keywordLower) {
// 也可以搜索创建时间
createdAtStr := queue.CreatedAt.Format("2006-01-02 15:04:05")
if !strings.Contains(createdAtStr, keyword) {
continue
}
}
}
filtered = append(filtered, queue)
}
// 按创建时间倒序排序
sort.Slice(filtered, func(i, j int) bool {
return filtered[i].CreatedAt.After(filtered[j].CreatedAt)
})
total = len(filtered)
// 分页
start := offset
if start > len(filtered) {
start = len(filtered)
}
end := start + limit
if end > len(filtered) {
end = len(filtered)
}
if start < len(filtered) {
queues = filtered[start:end]
}
}
return queues, total, nil
}
// LoadFromDB 从数据库加载所有队列
func (m *BatchTaskManager) LoadFromDB() error {
if m.db == nil {
@@ -534,7 +630,42 @@ func (m *BatchTaskManager) SetTaskCancel(queueID string, cancel context.CancelFu
}
}
// CancelQueue 取消队列
// PauseQueue 暂停队列
func (m *BatchTaskManager) PauseQueue(queueID string) bool {
m.mu.Lock()
queue, exists := m.queues[queueID]
if !exists {
m.mu.Unlock()
return false
}
if queue.Status != "running" {
m.mu.Unlock()
return false
}
queue.Status = "paused"
// 取消当前正在执行的任务(通过取消context)
if cancel, exists := m.taskCancels[queueID]; exists {
cancel()
delete(m.taskCancels, queueID)
}
m.mu.Unlock()
// 同步队列状态到数据库
if m.db != nil {
if err := m.db.UpdateBatchQueueStatus(queueID, "paused"); err != nil {
// 记录错误但继续(使用内存缓存)
}
}
return true
}
// CancelQueue 取消队列(保留此方法以保持向后兼容,但建议使用PauseQueue)
func (m *BatchTaskManager) CancelQueue(queueID string) bool {
m.mu.Lock()
+21
View File
@@ -6246,6 +6246,27 @@ header {
border-color: var(--accent-color);
}
.tasks-filters input[type="text"] {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
background: var(--bg-primary);
color: var(--text-primary);
width: 100%;
transition: all 0.2s;
}
.tasks-filters input[type="text"]:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1);
}
.tasks-filters input[type="text"]:hover {
border-color: var(--accent-color);
}
.tasks-batch-actions {
display: flex;
align-items: center;
+22 -14
View File
@@ -871,10 +871,6 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
// 渲染过程详情
function renderProcessDetails(messageId, processDetails) {
if (!processDetails || processDetails.length === 0) {
return;
}
const messageElement = document.getElementById(messageId);
if (!messageElement) {
return;
@@ -942,7 +938,7 @@ function renderProcessDetails(messageId, processDetails) {
}
}
// 创建时间线
// 创建时间线(即使没有processDetails也要创建,以便展开详情按钮能正常工作)
const timelineId = detailsId + '-timeline';
let timeline = document.getElementById(timelineId);
@@ -958,9 +954,19 @@ function renderProcessDetails(messageId, processDetails) {
detailsContainer.appendChild(contentDiv);
}
// 如果没有processDetails或为空,显示空状态
if (!processDetails || processDetails.length === 0) {
// 显示空状态提示
timeline.innerHTML = '<div class="progress-timeline-empty">暂无过程详情(可能执行过快或未触发详细事件)</div>';
// 默认折叠
timeline.classList.remove('expanded');
return;
}
// 清空时间线并重新渲染
timeline.innerHTML = '';
// 渲染每个过程详情事件
processDetails.forEach(detail => {
const eventType = detail.eventType || '';
@@ -1606,17 +1612,19 @@ async function loadConversation(conversationId) {
// 传递消息的创建时间
const messageId = addMessage(msg.role, displayContent, msg.mcpExecutionIds || [], null, msg.createdAt);
// 如果有过程详情,显示它们
if (msg.processDetails && msg.processDetails.length > 0 && msg.role === 'assistant') {
// 对于助手消息,总是渲染过程详情(即使没有processDetails也要显示展开详情按钮)
if (msg.role === 'assistant') {
// 延迟一下,确保消息已经渲染
setTimeout(() => {
renderProcessDetails(messageId, msg.processDetails);
// 检查是否有错误或取消事件,如果有,确保详情默认折叠
const hasErrorOrCancelled = msg.processDetails.some(d =>
d.eventType === 'error' || d.eventType === 'cancelled'
);
if (hasErrorOrCancelled) {
collapseAllProgressDetails(messageId, null);
renderProcessDetails(messageId, msg.processDetails || []);
// 如果有过程详情,检查是否有错误或取消事件,如果有,确保详情默认折叠
if (msg.processDetails && msg.processDetails.length > 0) {
const hasErrorOrCancelled = msg.processDetails.some(d =>
d.eventType === 'error' || d.eventType === 'cancelled'
);
if (hasErrorOrCancelled) {
collapseAllProgressDetails(messageId, null);
}
}
}, 100);
}
+218 -28
View File
@@ -706,7 +706,14 @@ window.clearTasksHistory = clearTasksHistory;
const batchQueuesState = {
queues: [],
currentQueueId: null,
refreshInterval: null
refreshInterval: null,
// 筛选和分页状态
filterStatus: 'all', // 'all', 'pending', 'running', 'paused', 'completed', 'cancelled'
searchKeyword: '',
currentPage: 1,
pageSize: 10,
total: 0,
totalPages: 1
};
// 显示批量导入模态框
@@ -802,19 +809,37 @@ async function createBatchQueue() {
}
// 加载批量任务队列列表
async function loadBatchQueues() {
async function loadBatchQueues(page) {
const section = document.getElementById('batch-queues-section');
if (!section) return;
// 如果指定了page,使用它;否则使用当前页
if (page !== undefined) {
batchQueuesState.currentPage = page;
}
// 构建查询参数
const params = new URLSearchParams();
params.append('page', batchQueuesState.currentPage.toString());
params.append('limit', batchQueuesState.pageSize.toString());
if (batchQueuesState.filterStatus && batchQueuesState.filterStatus !== 'all') {
params.append('status', batchQueuesState.filterStatus);
}
if (batchQueuesState.searchKeyword) {
params.append('keyword', batchQueuesState.searchKeyword);
}
try {
const response = await apiFetch('/api/batch-tasks');
const response = await apiFetch(`/api/batch-tasks?${params.toString()}`);
if (!response.ok) {
throw new Error('获取批量任务队列失败');
}
const result = await response.json();
batchQueuesState.queues = result.queues || [];
renderBatchQueues(batchQueuesState.queues);
batchQueuesState.total = result.total || 0;
batchQueuesState.totalPages = result.total_pages || 1;
renderBatchQueues();
} catch (error) {
console.error('加载批量任务队列失败:', error);
section.style.display = 'block';
@@ -825,26 +850,42 @@ async function loadBatchQueues() {
}
}
// 筛选批量任务队列
function filterBatchQueues() {
const statusFilter = document.getElementById('batch-queues-status-filter');
const searchInput = document.getElementById('batch-queues-search');
if (statusFilter) {
batchQueuesState.filterStatus = statusFilter.value;
}
if (searchInput) {
batchQueuesState.searchKeyword = searchInput.value.trim();
}
// 重置到第一页并重新加载
batchQueuesState.currentPage = 1;
loadBatchQueues(1);
}
// 渲染批量任务队列列表
function renderBatchQueues(queues) {
function renderBatchQueues() {
const section = document.getElementById('batch-queues-section');
const list = document.getElementById('batch-queues-list');
const pagination = document.getElementById('batch-queues-pagination');
if (!section || !list) return;
section.style.display = 'block';
const queues = batchQueuesState.queues;
if (queues.length === 0) {
list.innerHTML = '<div class="tasks-empty"><p>当前没有批量任务队列</p></div>';
if (pagination) pagination.style.display = 'none';
return;
}
// 按创建时间倒序排序
const sortedQueues = [...queues].sort((a, b) =>
new Date(b.createdAt) - new Date(a.createdAt)
);
list.innerHTML = sortedQueues.map(queue => {
list.innerHTML = queues.map(queue => {
const statusMap = {
'pending': { text: '待执行', class: 'batch-queue-status-pending' },
'running': { text: '执行中', class: 'batch-queue-status-running' },
@@ -874,7 +915,8 @@ function renderBatchQueues(queues) {
});
const progress = stats.total > 0 ? Math.round((stats.completed + stats.failed + stats.cancelled) / stats.total * 100) : 0;
const canDelete = queue.status === 'pending'; // 只有待执行状态的队列可以删除
// 允许删除待执行、已完成或已取消状态的队列
const canDelete = queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled';
return `
<div class="batch-queue-item" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
@@ -905,6 +947,134 @@ function renderBatchQueues(queues) {
</div>
`;
}).join('');
// 渲染分页控件
renderBatchQueuesPagination();
}
// 渲染批量任务队列分页控件
function renderBatchQueuesPagination() {
const paginationContainer = document.getElementById('batch-queues-pagination');
if (!paginationContainer) return;
const { currentPage, pageSize, total, totalPages } = batchQueuesState;
// 如果没有数据,不显示分页控件
if (total === 0) {
paginationContainer.innerHTML = '';
return;
}
// 即使只有一页,也显示分页信息(总数和每页条数选择器)
// 计算显示的页码范围
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);
}
}
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>`;
// 每页条数选择器
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>
`;
// 只有当有多页时才显示页码导航
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>';
}
paginationHTML += '</div>';
paginationContainer.innerHTML = paginationHTML;
}
// 跳转到指定页面
function goBatchQueuesPage(page) {
const { totalPages } = batchQueuesState;
if (page < 1 || page > totalPages) return;
loadBatchQueues(page);
// 滚动到列表顶部
const list = document.getElementById('batch-queues-list');
if (list) {
list.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
// 改变每页显示数量
function changeBatchQueuesPageSize() {
const pageSizeSelect = document.getElementById('batch-queues-page-size-pagination');
if (!pageSizeSelect) return;
const newPageSize = parseInt(pageSizeSelect.value, 10);
if (newPageSize && newPageSize > 0) {
batchQueuesState.pageSize = newPageSize;
batchQueuesState.currentPage = 1; // 重置到第一页
loadBatchQueues(1);
}
}
// 显示批量任务队列详情
@@ -934,22 +1104,39 @@ async function showBatchQueueDetail(queueId) {
}
// 更新按钮显示
const pauseBtn = document.getElementById('batch-queue-pause-btn');
if (addTaskBtn) {
addTaskBtn.style.display = queue.status === 'pending' ? 'inline-block' : 'none';
}
if (startBtn) {
// pending状态显示"开始执行"paused状态显示"继续执行"
startBtn.style.display = (queue.status === 'pending' || queue.status === 'paused') ? 'inline-block' : 'none';
if (startBtn && queue.status === 'paused') {
startBtn.textContent = '继续执行';
} else if (startBtn && queue.status === 'pending') {
startBtn.textContent = '开始执行';
}
}
if (cancelBtn) {
cancelBtn.style.display = (queue.status === 'running' || queue.status === 'paused') ? 'inline-block' : 'none';
if (pauseBtn) {
// running状态显示"暂停队列"
pauseBtn.style.display = queue.status === 'running' ? 'inline-block' : 'none';
}
if (deleteBtn) {
// 允许删除待执行、已完成或已取消状态的队列
deleteBtn.style.display = (queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled') ? 'inline-block' : 'none';
deleteBtn.style.display = (queue.status === 'pending' || queue.status === 'completed' || queue.status === 'cancelled' || queue.status === 'paused') ? 'inline-block' : 'none';
}
// 渲染任务列表
const statusMap = {
// 队列状态映射
const queueStatusMap = {
'pending': { text: '待执行', class: 'batch-queue-status-pending' },
'running': { text: '执行中', class: 'batch-queue-status-running' },
'paused': { text: '已暂停', class: 'batch-queue-status-paused' },
'completed': { text: '已完成', class: 'batch-queue-status-completed' },
'cancelled': { text: '已取消', class: 'batch-queue-status-cancelled' }
};
// 任务状态映射
const taskStatusMap = {
'pending': { text: '待执行', class: 'batch-task-status-pending' },
'running': { text: '执行中', class: 'batch-task-status-running' },
'completed': { text: '已完成', class: 'batch-task-status-completed' },
@@ -963,7 +1150,7 @@ async function showBatchQueueDetail(queueId) {
<strong>队列ID:</strong> <code>${escapeHtml(queue.id)}</code>
</div>
<div class="detail-item">
<strong>状态:</strong> <span class="batch-queue-status ${statusMap[queue.status]?.class || ''}">${statusMap[queue.status]?.text || queue.status}</span>
<strong>状态:</strong> <span class="batch-queue-status ${queueStatusMap[queue.status]?.class || ''}">${queueStatusMap[queue.status]?.text || queue.status}</span>
</div>
<div class="detail-item">
<strong>创建时间:</strong> ${new Date(queue.createdAt).toLocaleString('zh-CN')}
@@ -977,7 +1164,7 @@ async function showBatchQueueDetail(queueId) {
<div class="batch-queue-tasks-list">
<h4>任务列表</h4>
${queue.tasks.map((task, index) => {
const taskStatus = statusMap[task.status] || { text: task.status, class: 'batch-task-status-unknown' };
const taskStatus = taskStatusMap[task.status] || { text: task.status, class: 'batch-task-status-unknown' };
const canEdit = queue.status === 'pending' && task.status === 'pending';
const taskMessageEscaped = escapeHtml(task.message).replace(/'/g, "&#39;").replace(/"/g, "&quot;").replace(/\n/g, "\\n");
return `
@@ -1036,31 +1223,31 @@ async function startBatchQueue() {
}
}
// 取消批量任务队列
async function cancelBatchQueue() {
// 暂停批量任务队列
async function pauseBatchQueue() {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) return;
if (!confirm('确定要取消这个批量任务队列吗?当前正在执行的任务将被停止,后续任务也不会执行。')) {
if (!confirm('确定要暂停这个批量任务队列吗?当前正在执行的任务将被停止,后续任务将保留待执行状态。')) {
return;
}
try {
const response = await apiFetch(`/api/batch-tasks/${queueId}/cancel`, {
const response = await apiFetch(`/api/batch-tasks/${queueId}/pause`, {
method: 'POST',
});
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '取消批量任务失败');
throw new Error(result.error || '暂停批量任务失败');
}
// 刷新详情
showBatchQueueDetail(queueId);
refreshBatchQueues();
} catch (error) {
console.error('取消批量任务失败:', error);
alert('取消批量任务失败: ' + error.message);
console.error('暂停批量任务失败:', error);
alert('暂停批量任务失败: ' + error.message);
}
}
@@ -1158,7 +1345,7 @@ function stopBatchQueueRefresh() {
// 刷新批量任务队列列表
async function refreshBatchQueues() {
await loadBatchQueues();
await loadBatchQueues(batchQueuesState.currentPage);
}
// 查看批量任务的对话
@@ -1487,7 +1674,7 @@ window.closeBatchImportModal = closeBatchImportModal;
window.createBatchQueue = createBatchQueue;
window.showBatchQueueDetail = showBatchQueueDetail;
window.startBatchQueue = startBatchQueue;
window.cancelBatchQueue = cancelBatchQueue;
window.pauseBatchQueue = pauseBatchQueue;
window.deleteBatchQueue = deleteBatchQueue;
window.closeBatchQueueDetailModal = closeBatchQueueDetailModal;
window.refreshBatchQueues = refreshBatchQueues;
@@ -1496,6 +1683,9 @@ window.editBatchTask = editBatchTask;
window.editBatchTaskFromElement = editBatchTaskFromElement;
window.closeEditBatchTaskModal = closeEditBatchTaskModal;
window.saveBatchTask = saveBatchTask;
window.filterBatchQueues = filterBatchQueues;
window.goBatchQueuesPage = goBatchQueuesPage;
window.changeBatchQueuesPageSize = changeBatchQueuesPageSize;
window.showAddBatchTaskModal = showAddBatchTaskModal;
window.closeAddBatchTaskModal = closeAddBatchTaskModal;
window.saveAddBatchTask = saveAddBatchTask;
+22 -1
View File
@@ -571,7 +571,28 @@
<div class="batch-queues-header">
<h3>批量任务队列</h3>
</div>
<!-- 筛选控件 -->
<div class="batch-queues-filters tasks-filters">
<label>
<span>状态筛选</span>
<select id="batch-queues-status-filter" onchange="filterBatchQueues()">
<option value="all">全部</option>
<option value="pending">待执行</option>
<option value="running">执行中</option>
<option value="paused">已暂停</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
</label>
<label style="flex: 1; max-width: 300px;">
<span>搜索队列ID或创建时间</span>
<input type="text" id="batch-queues-search" placeholder="输入关键字搜索..."
oninput="filterBatchQueues()">
</label>
</div>
<div id="batch-queues-list" class="batch-queues-list"></div>
<!-- 分页控件 -->
<div id="batch-queues-pagination"></div>
</div>
</div>
</div>
@@ -1166,7 +1187,7 @@
<div class="modal-header-actions">
<button class="btn-secondary" id="batch-queue-add-task-btn" onclick="showAddBatchTaskModal()" style="display: none;">添加任务</button>
<button class="btn-primary" id="batch-queue-start-btn" onclick="startBatchQueue()" style="display: none;">开始执行</button>
<button class="btn-secondary btn-danger" id="batch-queue-cancel-btn" onclick="cancelBatchQueue()" style="display: none;">取消队列</button>
<button class="btn-secondary" id="batch-queue-pause-btn" onclick="pauseBatchQueue()" style="display: none;">暂停队列</button>
<button class="btn-secondary btn-danger" id="batch-queue-delete-btn" onclick="deleteBatchQueue()" style="display: none;">删除队列</button>
</div>
<span class="modal-close" onclick="closeBatchQueueDetailModal()">&times;</span>