mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-30 21:20:12 +02:00
Add files via upload
This commit is contained in:
8 files changed
+450
-92
No files matched your search
@@ -757,6 +757,7 @@ func setupRoutes(
|
|||||||
// 监控
|
// 监控
|
||||||
protected.GET("/monitor", monitorHandler.Monitor)
|
protected.GET("/monitor", monitorHandler.Monitor)
|
||||||
protected.GET("/monitor/execution/:id", monitorHandler.GetExecution)
|
protected.GET("/monitor/execution/:id", monitorHandler.GetExecution)
|
||||||
|
protected.POST("/monitor/execution/:id/cancel", monitorHandler.CancelExecution)
|
||||||
protected.POST("/monitor/executions/names", monitorHandler.BatchGetToolNames)
|
protected.POST("/monitor/executions/names", monitorHandler.BatchGetToolNames)
|
||||||
protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution)
|
protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution)
|
||||||
protected.DELETE("/monitor/executions", monitorHandler.DeleteExecutions)
|
protected.DELETE("/monitor/executions", monitorHandler.DeleteExecutions)
|
||||||
|
|||||||
@@ -1717,6 +1717,8 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
|||||||
func (h *AgentHandler) CancelAgentLoop(c *gin.Context) {
|
func (h *AgentHandler) CancelAgentLoop(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
ConversationID string `json:"conversationId" binding:"required"`
|
ConversationID string `json:"conversationId" binding:"required"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
ContinueAfter bool `json:"continueAfter,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -1724,7 +1726,23 @@ func (h *AgentHandler) CancelAgentLoop(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := h.tasks.CancelTask(req.ConversationID, ErrTaskCancelled)
|
if req.ContinueAfter && strings.TrimSpace(req.Reason) == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "continueAfter 为 true 时必须提供非空的 reason(中断说明)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cause error = ErrTaskCancelled
|
||||||
|
msg := "已提交取消请求,任务将在当前步骤完成后停止。"
|
||||||
|
if req.ContinueAfter {
|
||||||
|
if !h.tasks.SetInterruptContinueReason(req.ConversationID, req.Reason) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "未找到正在执行的任务,无法提交中断说明"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cause = ErrUserInterruptContinue
|
||||||
|
msg = "已提交中断说明,当前步骤结束后将写入对话并继续迭代。"
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := h.tasks.CancelTask(req.ConversationID, cause)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("取消任务失败", zap.Error(err))
|
h.logger.Error("取消任务失败", zap.Error(err))
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -1737,9 +1755,11 @@ func (h *AgentHandler) CancelAgentLoop(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": "cancelling",
|
"status": "cancelling",
|
||||||
"conversationId": req.ConversationID,
|
"conversationId": req.ConversationID,
|
||||||
"message": "已提交取消请求,任务将在当前步骤完成后停止。",
|
"message": msg,
|
||||||
|
"continueAfter": req.ContinueAfter,
|
||||||
|
"interruptWithNote": req.ContinueAfter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,11 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
|||||||
var sseWriteMu sync.Mutex
|
var sseWriteMu sync.Mutex
|
||||||
var ssePublishConversationID string
|
var ssePublishConversationID string
|
||||||
sendEvent := func(eventType, message string, data interface{}) {
|
sendEvent := func(eventType, message string, data interface{}) {
|
||||||
if eventType == "error" && baseCtx != nil && errors.Is(context.Cause(baseCtx), ErrTaskCancelled) {
|
if eventType == "error" && baseCtx != nil {
|
||||||
return
|
cause := context.Cause(baseCtx)
|
||||||
|
if errors.Is(cause, ErrTaskCancelled) || errors.Is(cause, ErrUserInterruptContinue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ev := StreamEvent{Type: eventType, Message: message, Data: data}
|
ev := StreamEvent{Type: eventType, Message: message, Data: data}
|
||||||
b, errMarshal := json.Marshal(ev)
|
b, errMarshal := json.Marshal(ev)
|
||||||
@@ -114,33 +117,10 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cancelWithCause context.CancelCauseFunc
|
var cancelWithCause context.CancelCauseFunc
|
||||||
baseCtx, cancelWithCause = context.WithCancelCause(context.Background())
|
firstRun := true
|
||||||
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
|
curFinalMessage := prep.FinalMessage
|
||||||
defer timeoutCancel()
|
curHistory := prep.History
|
||||||
defer cancelWithCause(nil)
|
roleTools := prep.RoleTools
|
||||||
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, conversationID, assistantMessageID, sendEvent)
|
|
||||||
taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
|
|
||||||
return h.interceptHITLForEinoTool(ctx, cancelWithCause, conversationID, assistantMessageID, sendEvent, toolName, arguments)
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, err := h.tasks.StartTask(conversationID, req.Message, cancelWithCause); err != nil {
|
|
||||||
var errorMsg string
|
|
||||||
if errors.Is(err, ErrTaskAlreadyRunning) {
|
|
||||||
errorMsg = "⚠️ 当前会话已有任务正在执行中,请等待当前任务完成或点击「停止任务」后再尝试。"
|
|
||||||
sendEvent("error", errorMsg, map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"errorType": "task_already_running",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
errorMsg = "❌ 无法启动任务: " + err.Error()
|
|
||||||
sendEvent("error", errorMsg, nil)
|
|
||||||
}
|
|
||||||
if assistantMessageID != "" {
|
|
||||||
_, _ = h.db.Exec("UPDATE messages SET content = ?, updated_at = ? WHERE id = ?", errorMsg, time.Now(), assistantMessageID)
|
|
||||||
}
|
|
||||||
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
taskStatus := "completed"
|
taskStatus := "completed"
|
||||||
defer h.tasks.FinishTask(conversationID, taskStatus)
|
defer h.tasks.FinishTask(conversationID, taskStatus)
|
||||||
@@ -161,22 +141,108 @@ func (h *AgentHandler) EinoSingleAgentLoopStream(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, runErr := multiagent.RunEinoSingleChatModelAgent(
|
var result *multiagent.RunResult
|
||||||
taskCtx,
|
var runErr error
|
||||||
h.config,
|
|
||||||
&h.config.MultiAgent,
|
for {
|
||||||
h.agent,
|
baseCtx, cancelWithCause = context.WithCancelCause(context.Background())
|
||||||
h.logger,
|
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
|
||||||
conversationID,
|
|
||||||
prep.FinalMessage,
|
if firstRun {
|
||||||
prep.History,
|
if _, err := h.tasks.StartTask(conversationID, req.Message, cancelWithCause); err != nil {
|
||||||
prep.RoleTools,
|
var errorMsg string
|
||||||
progressCallback,
|
if errors.Is(err, ErrTaskAlreadyRunning) {
|
||||||
)
|
errorMsg = "⚠️ 当前会话已有任务正在执行中,请等待当前任务完成或点击「停止任务」后再尝试。"
|
||||||
|
sendEvent("error", errorMsg, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"errorType": "task_already_running",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
errorMsg = "❌ 无法启动任务: " + err.Error()
|
||||||
|
sendEvent("error", errorMsg, nil)
|
||||||
|
}
|
||||||
|
if assistantMessageID != "" {
|
||||||
|
_, _ = h.db.Exec("UPDATE messages SET content = ?, updated_at = ? WHERE id = ?", errorMsg, time.Now(), assistantMessageID)
|
||||||
|
}
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
timeoutCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
firstRun = false
|
||||||
|
} else {
|
||||||
|
if err := h.tasks.ResetTaskCancelForContinue(conversationID, cancelWithCause); err != nil {
|
||||||
|
h.logger.Error("续跑任务时重置 cancel 失败", zap.Error(err))
|
||||||
|
taskStatus = "failed"
|
||||||
|
sendEvent("error", err.Error(), nil)
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
timeoutCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, conversationID, assistantMessageID, sendEvent)
|
||||||
|
taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
|
||||||
|
return h.interceptHITLForEinoTool(ctx, cancelWithCause, conversationID, assistantMessageID, sendEvent, toolName, arguments)
|
||||||
|
})
|
||||||
|
|
||||||
|
result, runErr = multiagent.RunEinoSingleChatModelAgent(
|
||||||
|
taskCtx,
|
||||||
|
h.config,
|
||||||
|
&h.config.MultiAgent,
|
||||||
|
h.agent,
|
||||||
|
h.logger,
|
||||||
|
conversationID,
|
||||||
|
curFinalMessage,
|
||||||
|
curHistory,
|
||||||
|
roleTools,
|
||||||
|
progressCallback,
|
||||||
|
)
|
||||||
|
timeoutCancel()
|
||||||
|
|
||||||
|
if runErr == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
if runErr != nil {
|
|
||||||
h.persistEinoAgentTraceForResume(conversationID, result)
|
h.persistEinoAgentTraceForResume(conversationID, result)
|
||||||
cause := context.Cause(baseCtx)
|
cause := context.Cause(baseCtx)
|
||||||
|
if errors.Is(cause, ErrUserInterruptContinue) {
|
||||||
|
reason := h.tasks.TakeInterruptContinueReason(conversationID)
|
||||||
|
prepNext, perr := h.prepareSessionAfterUserInterrupt(conversationID, assistantMessageID, reason, roleTools)
|
||||||
|
if perr != nil {
|
||||||
|
h.logger.Error("准备中断后续跑失败", zap.Error(perr))
|
||||||
|
taskStatus = "failed"
|
||||||
|
h.tasks.UpdateTaskStatus(conversationID, taskStatus)
|
||||||
|
errMsg := "中断后续跑失败: " + perr.Error()
|
||||||
|
if assistantMessageID != "" {
|
||||||
|
_, _ = h.db.Exec("UPDATE messages SET content = ?, updated_at = ? WHERE id = ?", errMsg, time.Now(), assistantMessageID)
|
||||||
|
}
|
||||||
|
sendEvent("error", errMsg, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"messageId": assistantMessageID,
|
||||||
|
})
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assistantMessageID = prepNext.AssistantMessageID
|
||||||
|
curFinalMessage = prepNext.FinalMessage
|
||||||
|
curHistory = prepNext.History
|
||||||
|
if prepNext.UserMessageID != "" {
|
||||||
|
sendEvent("message_saved", "", map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"userMessageId": prepNext.UserMessageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sendEvent("user_interrupt_continue", reason, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"reason": reason,
|
||||||
|
"messageId": assistantMessageID,
|
||||||
|
})
|
||||||
|
sendEvent("progress", "已接收中断说明,继续迭代...", map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if errors.Is(cause, ErrTaskCancelled) {
|
if errors.Is(cause, ErrTaskCancelled) {
|
||||||
taskStatus = "cancelled"
|
taskStatus = "cancelled"
|
||||||
h.tasks.UpdateTaskStatus(conversationID, taskStatus)
|
h.tasks.UpdateTaskStatus(conversationID, taskStatus)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -245,6 +248,37 @@ func (h *MonitorHandler) GetExecution(c *gin.Context) {
|
|||||||
c.JSON(http.StatusNotFound, gin.H{"error": "执行记录未找到"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "执行记录未找到"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancelExecution 手动取消进行中的 MCP 工具调用(仅取消该次 tools/call 的上下文,不停止整条 Agent / 迭代任务)
|
||||||
|
// 请求体可选 JSON:{ "note": "用户说明" },将与工具已返回输出合并交给模型(含「用户终止说明」标题块,与命令行原文区分)。
|
||||||
|
func (h *MonitorHandler) CancelExecution(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "执行记录ID不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
note := ""
|
||||||
|
dec := json.NewDecoder(c.Request.Body)
|
||||||
|
var body struct {
|
||||||
|
Note string `json:"note"`
|
||||||
|
}
|
||||||
|
if err := dec.Decode(&body); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请求体须为 JSON,例如 {\"note\":\"说明\"},可为空对象"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
note = strings.TrimSpace(body.Note)
|
||||||
|
if h.mcpServer.CancelToolExecutionWithNote(id, note) {
|
||||||
|
h.logger.Info("已请求取消 MCP 工具执行", zap.String("executionId", id), zap.String("source", "internal"), zap.Bool("hasNote", note != ""))
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "已发送终止信号", "executionId": id})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.externalMCPMgr != nil && h.externalMCPMgr.CancelToolExecutionWithNote(id, note) {
|
||||||
|
h.logger.Info("已请求取消 MCP 工具执行", zap.String("executionId", id), zap.String("source", "external"), zap.Bool("hasNote", note != ""))
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "已发送终止信号", "executionId": id})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "未找到进行中的工具执行,或该任务已结束"})
|
||||||
|
}
|
||||||
|
|
||||||
// BatchGetToolNames 批量获取工具执行的工具名称(消除前端 N+1 请求)
|
// BatchGetToolNames 批量获取工具执行的工具名称(消除前端 N+1 请求)
|
||||||
func (h *MonitorHandler) BatchGetToolNames(c *gin.Context) {
|
func (h *MonitorHandler) BatchGetToolNames(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
@@ -317,7 +351,7 @@ func (h *MonitorHandler) DeleteExecution(c *gin.Context) {
|
|||||||
totalCalls := 1
|
totalCalls := 1
|
||||||
successCalls := 0
|
successCalls := 0
|
||||||
failedCalls := 0
|
failedCalls := 0
|
||||||
if exec.Status == "failed" {
|
if exec.Status == "failed" || exec.Status == "cancelled" {
|
||||||
failedCalls = 1
|
failedCalls = 1
|
||||||
} else if exec.Status == "completed" {
|
} else if exec.Status == "completed" {
|
||||||
successCalls = 1
|
successCalls = 1
|
||||||
@@ -381,7 +415,7 @@ func (h *MonitorHandler) DeleteExecutions(c *gin.Context) {
|
|||||||
|
|
||||||
stats := toolStats[exec.ToolName]
|
stats := toolStats[exec.ToolName]
|
||||||
stats.totalCalls++
|
stats.totalCalls++
|
||||||
if exec.Status == "failed" {
|
if exec.Status == "failed" || exec.Status == "cancelled" {
|
||||||
stats.failedCalls++
|
stats.failedCalls++
|
||||||
} else if exec.Status == "completed" {
|
} else if exec.Status == "completed" {
|
||||||
stats.successCalls++
|
stats.successCalls++
|
||||||
|
|||||||
+112
-44
@@ -60,8 +60,11 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
|||||||
sendEvent := func(eventType, message string, data interface{}) {
|
sendEvent := func(eventType, message string, data interface{}) {
|
||||||
// 用户主动停止时,Eino 可能仍会并发上报 eventType=="error"。
|
// 用户主动停止时,Eino 可能仍会并发上报 eventType=="error"。
|
||||||
// 为避免 UI 看到“取消错误 + cancelled 文案”两条回复,这里直接丢弃取消对应的 error。
|
// 为避免 UI 看到“取消错误 + cancelled 文案”两条回复,这里直接丢弃取消对应的 error。
|
||||||
if eventType == "error" && baseCtx != nil && errors.Is(context.Cause(baseCtx), ErrTaskCancelled) {
|
if eventType == "error" && baseCtx != nil {
|
||||||
return
|
cause := context.Cause(baseCtx)
|
||||||
|
if errors.Is(cause, ErrTaskCancelled) || errors.Is(cause, ErrUserInterruptContinue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ev := StreamEvent{Type: eventType, Message: message, Data: data}
|
ev := StreamEvent{Type: eventType, Message: message, Data: data}
|
||||||
b, errMarshal := json.Marshal(ev)
|
b, errMarshal := json.Marshal(ev)
|
||||||
@@ -130,33 +133,12 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
baseCtx, cancelWithCause := context.WithCancelCause(context.Background())
|
var cancelWithCause context.CancelCauseFunc
|
||||||
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
|
firstRun := true
|
||||||
defer timeoutCancel()
|
curFinalMessage := prep.FinalMessage
|
||||||
defer cancelWithCause(nil)
|
curHistory := prep.History
|
||||||
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, conversationID, assistantMessageID, sendEvent)
|
roleTools := prep.RoleTools
|
||||||
taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
|
orch := strings.TrimSpace(req.Orchestration)
|
||||||
return h.interceptHITLForEinoTool(ctx, cancelWithCause, conversationID, assistantMessageID, sendEvent, toolName, arguments)
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, err := h.tasks.StartTask(conversationID, req.Message, cancelWithCause); err != nil {
|
|
||||||
var errorMsg string
|
|
||||||
if errors.Is(err, ErrTaskAlreadyRunning) {
|
|
||||||
errorMsg = "⚠️ 当前会话已有任务正在执行中,请等待当前任务完成或点击「停止任务」后再尝试。"
|
|
||||||
sendEvent("error", errorMsg, map[string]interface{}{
|
|
||||||
"conversationId": conversationID,
|
|
||||||
"errorType": "task_already_running",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
errorMsg = "❌ 无法启动任务: " + err.Error()
|
|
||||||
sendEvent("error", errorMsg, nil)
|
|
||||||
}
|
|
||||||
if assistantMessageID != "" {
|
|
||||||
_, _ = h.db.Exec("UPDATE messages SET content = ?, updated_at = ? WHERE id = ?", errorMsg, time.Now(), assistantMessageID)
|
|
||||||
}
|
|
||||||
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
taskStatus := "completed"
|
taskStatus := "completed"
|
||||||
defer h.tasks.FinishTask(conversationID, taskStatus)
|
defer h.tasks.FinishTask(conversationID, taskStatus)
|
||||||
@@ -169,24 +151,110 @@ func (h *AgentHandler) MultiAgentLoopStream(c *gin.Context) {
|
|||||||
go sseKeepalive(c, stopKeepalive, &sseWriteMu)
|
go sseKeepalive(c, stopKeepalive, &sseWriteMu)
|
||||||
defer close(stopKeepalive)
|
defer close(stopKeepalive)
|
||||||
|
|
||||||
result, runErr := multiagent.RunDeepAgent(
|
var result *multiagent.RunResult
|
||||||
taskCtx,
|
var runErr error
|
||||||
h.config,
|
|
||||||
&h.config.MultiAgent,
|
for {
|
||||||
h.agent,
|
baseCtx, cancelWithCause = context.WithCancelCause(context.Background())
|
||||||
h.logger,
|
taskCtx, timeoutCancel := context.WithTimeout(baseCtx, 600*time.Minute)
|
||||||
conversationID,
|
|
||||||
prep.FinalMessage,
|
if firstRun {
|
||||||
prep.History,
|
if _, err := h.tasks.StartTask(conversationID, req.Message, cancelWithCause); err != nil {
|
||||||
prep.RoleTools,
|
var errorMsg string
|
||||||
progressCallback,
|
if errors.Is(err, ErrTaskAlreadyRunning) {
|
||||||
h.agentsMarkdownDir,
|
errorMsg = "⚠️ 当前会话已有任务正在执行中,请等待当前任务完成或点击「停止任务」后再尝试。"
|
||||||
strings.TrimSpace(req.Orchestration),
|
sendEvent("error", errorMsg, map[string]interface{}{
|
||||||
)
|
"conversationId": conversationID,
|
||||||
|
"errorType": "task_already_running",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
errorMsg = "❌ 无法启动任务: " + err.Error()
|
||||||
|
sendEvent("error", errorMsg, nil)
|
||||||
|
}
|
||||||
|
if assistantMessageID != "" {
|
||||||
|
_, _ = h.db.Exec("UPDATE messages SET content = ?, updated_at = ? WHERE id = ?", errorMsg, time.Now(), assistantMessageID)
|
||||||
|
}
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
timeoutCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
firstRun = false
|
||||||
|
} else {
|
||||||
|
if err := h.tasks.ResetTaskCancelForContinue(conversationID, cancelWithCause); err != nil {
|
||||||
|
h.logger.Error("续跑任务时重置 cancel 失败", zap.Error(err))
|
||||||
|
taskStatus = "failed"
|
||||||
|
sendEvent("error", err.Error(), nil)
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
timeoutCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, conversationID, assistantMessageID, sendEvent)
|
||||||
|
taskCtx = multiagent.WithHITLToolInterceptor(taskCtx, func(ctx context.Context, toolName, arguments string) (string, error) {
|
||||||
|
return h.interceptHITLForEinoTool(ctx, cancelWithCause, conversationID, assistantMessageID, sendEvent, toolName, arguments)
|
||||||
|
})
|
||||||
|
|
||||||
|
result, runErr = multiagent.RunDeepAgent(
|
||||||
|
taskCtx,
|
||||||
|
h.config,
|
||||||
|
&h.config.MultiAgent,
|
||||||
|
h.agent,
|
||||||
|
h.logger,
|
||||||
|
conversationID,
|
||||||
|
curFinalMessage,
|
||||||
|
curHistory,
|
||||||
|
roleTools,
|
||||||
|
progressCallback,
|
||||||
|
h.agentsMarkdownDir,
|
||||||
|
orch,
|
||||||
|
)
|
||||||
|
timeoutCancel()
|
||||||
|
|
||||||
|
if runErr == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
if runErr != nil {
|
|
||||||
h.persistEinoAgentTraceForResume(conversationID, result)
|
h.persistEinoAgentTraceForResume(conversationID, result)
|
||||||
cause := context.Cause(baseCtx)
|
cause := context.Cause(baseCtx)
|
||||||
|
if errors.Is(cause, ErrUserInterruptContinue) {
|
||||||
|
reason := h.tasks.TakeInterruptContinueReason(conversationID)
|
||||||
|
prepNext, perr := h.prepareSessionAfterUserInterrupt(conversationID, assistantMessageID, reason, roleTools)
|
||||||
|
if perr != nil {
|
||||||
|
h.logger.Error("准备中断后续跑失败", zap.Error(perr))
|
||||||
|
taskStatus = "failed"
|
||||||
|
h.tasks.UpdateTaskStatus(conversationID, taskStatus)
|
||||||
|
errMsg := "中断后续跑失败: " + perr.Error()
|
||||||
|
if assistantMessageID != "" {
|
||||||
|
_, _ = h.db.Exec("UPDATE messages SET content = ?, updated_at = ? WHERE id = ?", errMsg, time.Now(), assistantMessageID)
|
||||||
|
}
|
||||||
|
sendEvent("error", errMsg, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"messageId": assistantMessageID,
|
||||||
|
})
|
||||||
|
sendEvent("done", "", map[string]interface{}{"conversationId": conversationID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assistantMessageID = prepNext.AssistantMessageID
|
||||||
|
curFinalMessage = prepNext.FinalMessage
|
||||||
|
curHistory = prepNext.History
|
||||||
|
if prepNext.UserMessageID != "" {
|
||||||
|
sendEvent("message_saved", "", map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"userMessageId": prepNext.UserMessageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sendEvent("user_interrupt_continue", reason, map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
"reason": reason,
|
||||||
|
"messageId": assistantMessageID,
|
||||||
|
})
|
||||||
|
sendEvent("progress", "已接收中断说明,继续迭代...", map[string]interface{}{
|
||||||
|
"conversationId": conversationID,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if errors.Is(cause, ErrTaskCancelled) {
|
if errors.Is(cause, ErrTaskCancelled) {
|
||||||
taskStatus = "cancelled"
|
taskStatus = "cancelled"
|
||||||
h.tasks.UpdateTaskStatus(conversationID, taskStatus)
|
h.tasks.UpdateTaskStatus(conversationID, taskStatus)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"cyberstrike-ai/internal/agent"
|
"cyberstrike-ai/internal/agent"
|
||||||
"cyberstrike-ai/internal/database"
|
"cyberstrike-ai/internal/database"
|
||||||
@@ -142,3 +143,64 @@ func (h *AgentHandler) prepareMultiAgentSession(req *ChatRequest) (*multiAgentPr
|
|||||||
UserMessageID: userMessageID,
|
UserMessageID: userMessageID,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareSessionAfterUserInterrupt 用户「中断并说明」后:结束当前助手占位、写入用户说明、新建助手占位,并生成下一轮 Run 所需的 History + FinalMessage。
|
||||||
|
func (h *AgentHandler) prepareSessionAfterUserInterrupt(conversationID, prevAssistantMessageID, reason string, roleTools []string) (*multiAgentPrepared, error) {
|
||||||
|
if strings.TrimSpace(conversationID) == "" {
|
||||||
|
return nil, fmt.Errorf("conversationId 为空")
|
||||||
|
}
|
||||||
|
if _, err := h.db.GetConversation(conversationID); err != nil {
|
||||||
|
return nil, fmt.Errorf("对话不存在")
|
||||||
|
}
|
||||||
|
note := "(已根据用户说明中断当前步骤,正在继续迭代。)"
|
||||||
|
if prevAssistantMessageID != "" {
|
||||||
|
if _, err := h.db.Exec("UPDATE messages SET content = ?, updated_at = ? WHERE id = ?", note, time.Now(), prevAssistantMessageID); err != nil {
|
||||||
|
return nil, fmt.Errorf("更新助手消息失败: %w", err)
|
||||||
|
}
|
||||||
|
r := strings.TrimSpace(reason)
|
||||||
|
detail := "用户中断并说明"
|
||||||
|
if r != "" {
|
||||||
|
detail += ":" + r
|
||||||
|
}
|
||||||
|
_ = h.db.AddProcessDetail(prevAssistantMessageID, conversationID, "user_interrupt", detail, map[string]interface{}{
|
||||||
|
"reason": r,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
userContent := fmt.Sprintf("【用户中断说明】%s\n\n请根据以上说明调整并继续任务。", strings.TrimSpace(reason))
|
||||||
|
if strings.TrimSpace(reason) == "" {
|
||||||
|
userContent = "【用户中断说明】(未填写具体原因)\n\n请根据情况调整并继续任务。"
|
||||||
|
}
|
||||||
|
userMsgRow, err := h.db.AddMessage(conversationID, "user", userContent, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("保存用户消息失败: %w", err)
|
||||||
|
}
|
||||||
|
assistantMsg, err := h.db.AddMessage(conversationID, "assistant", "处理中...", nil)
|
||||||
|
if err != nil || assistantMsg == nil {
|
||||||
|
return nil, fmt.Errorf("创建助手占位失败: %w", err)
|
||||||
|
}
|
||||||
|
msgs, err := h.db.GetMessages(conversationID)
|
||||||
|
if err != nil || len(msgs) < 2 {
|
||||||
|
return nil, fmt.Errorf("读取消息历史失败或消息不足")
|
||||||
|
}
|
||||||
|
histMsgs := msgs[:len(msgs)-2]
|
||||||
|
agentHistory := make([]agent.ChatMessage, 0, len(histMsgs))
|
||||||
|
for _, msg := range histMsgs {
|
||||||
|
agentHistory = append(agentHistory, agent.ChatMessage{
|
||||||
|
Role: msg.Role,
|
||||||
|
Content: msg.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
userMessageID := ""
|
||||||
|
if userMsgRow != nil {
|
||||||
|
userMessageID = userMsgRow.ID
|
||||||
|
}
|
||||||
|
return &multiAgentPrepared{
|
||||||
|
ConversationID: conversationID,
|
||||||
|
CreatedNew: false,
|
||||||
|
History: agentHistory,
|
||||||
|
FinalMessage: userContent,
|
||||||
|
RoleTools: roleTools,
|
||||||
|
AssistantMessageID: assistantMsg.ID,
|
||||||
|
UserMessageID: userMessageID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -461,6 +461,14 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "对话ID",
|
"description": "对话ID",
|
||||||
},
|
},
|
||||||
|
"reason": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "中断说明;与 continueAfter 同时为真时必填,将写入对话并由同一会话流式迭代继续",
|
||||||
|
},
|
||||||
|
"continueAfter": map[string]interface{}{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "为 true 时取消当前运行步骤并注入 reason 后继续迭代(非彻底停止)",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"AgentTask": map[string]interface{}{
|
"AgentTask": map[string]interface{}{
|
||||||
@@ -3318,6 +3326,55 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/api/monitor/execution/{id}/cancel": map[string]interface{}{
|
||||||
|
"post": map[string]interface{}{
|
||||||
|
"tags": []string{"监控"},
|
||||||
|
"summary": "取消进行中的工具执行",
|
||||||
|
"description": "对当前进程内正在执行的 MCP 工具调用发送 context 取消信号;上层对话/多步任务可继续。若执行已结束或未在本进程内运行则返回 404。",
|
||||||
|
"operationId": "cancelExecution",
|
||||||
|
"parameters": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "执行ID",
|
||||||
|
"schema": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"requestBody": map[string]interface{}{
|
||||||
|
"required": false,
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"application/json": map[string]interface{}{
|
||||||
|
"schema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"note": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "可选。非空时与工具已返回输出合并交给大模型,并带有「用户终止说明」标题块以便与命令行原文区分",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"responses": map[string]interface{}{
|
||||||
|
"200": map[string]interface{}{
|
||||||
|
"description": "已发送终止信号",
|
||||||
|
},
|
||||||
|
"400": map[string]interface{}{
|
||||||
|
"description": "请求体不是合法 JSON",
|
||||||
|
},
|
||||||
|
"404": map[string]interface{}{
|
||||||
|
"description": "未找到进行中的工具执行",
|
||||||
|
},
|
||||||
|
"401": map[string]interface{}{
|
||||||
|
"description": "未授权",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"/api/monitor/executions": map[string]interface{}{
|
"/api/monitor/executions": map[string]interface{}{
|
||||||
"delete": map[string]interface{}{
|
"delete": map[string]interface{}{
|
||||||
"tags": []string{"监控"},
|
"tags": []string{"监控"},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -10,6 +11,9 @@ import (
|
|||||||
// ErrTaskCancelled 用户取消任务的错误
|
// ErrTaskCancelled 用户取消任务的错误
|
||||||
var ErrTaskCancelled = errors.New("agent task cancelled by user")
|
var ErrTaskCancelled = errors.New("agent task cancelled by user")
|
||||||
|
|
||||||
|
// ErrUserInterruptContinue 用户在进度条上「中断并说明」:取消当前运行步骤,将说明写入对话并继续迭代(与 ErrTaskCancelled 区分)
|
||||||
|
var ErrUserInterruptContinue = errors.New("user interrupt with continue")
|
||||||
|
|
||||||
// ErrTaskAlreadyRunning 会话已有任务正在执行
|
// ErrTaskAlreadyRunning 会话已有任务正在执行
|
||||||
var ErrTaskAlreadyRunning = errors.New("agent task already running for conversation")
|
var ErrTaskAlreadyRunning = errors.New("agent task already running for conversation")
|
||||||
|
|
||||||
@@ -21,6 +25,9 @@ type AgentTask struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CancellingAt time.Time `json:"-"` // 进入 cancelling 状态的时间,用于清理长时间卡住的任务
|
CancellingAt time.Time `json:"-"` // 进入 cancelling 状态的时间,用于清理长时间卡住的任务
|
||||||
|
|
||||||
|
// InterruptContinueReason 由 /api/agent-loop/cancel 在 continueAfter 时写入,Run 返回后由 handler 取出并清空
|
||||||
|
InterruptContinueReason string `json:"-"`
|
||||||
|
|
||||||
cancel func(error)
|
cancel func(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +147,49 @@ func (m *AgentTaskManager) StartTask(conversationID, message string, cancel cont
|
|||||||
return task, nil
|
return task, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetInterruptContinueReason 在发起 ErrUserInterruptContinue 取消前写入用户说明(须任务仍存在)。
|
||||||
|
func (m *AgentTaskManager) SetInterruptContinueReason(conversationID, reason string) bool {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
task, ok := m.tasks[conversationID]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
task.InterruptContinueReason = strings.TrimSpace(reason)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TakeInterruptContinueReason 取出并清空用户中断说明。
|
||||||
|
func (m *AgentTaskManager) TakeInterruptContinueReason(conversationID string) string {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
task, ok := m.tasks[conversationID]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
r := task.InterruptContinueReason
|
||||||
|
task.InterruptContinueReason = ""
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetTaskCancelForContinue 在一次「中断并继续」后恢复任务为 running 并绑定新的 cancel(同一会话同一条 HTTP 流内续跑)。
|
||||||
|
func (m *AgentTaskManager) ResetTaskCancelForContinue(conversationID string, cancel context.CancelCauseFunc) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
task, ok := m.tasks[conversationID]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("no active task")
|
||||||
|
}
|
||||||
|
task.cancel = func(err error) {
|
||||||
|
if cancel != nil {
|
||||||
|
cancel(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.Status = "running"
|
||||||
|
task.CancellingAt = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CancelTask 取消指定会话的任务。若任务已在取消中,仍返回 (true, nil) 以便接口幂等、前端不报错。
|
// CancelTask 取消指定会话的任务。若任务已在取消中,仍返回 (true, nil) 以便接口幂等、前端不报错。
|
||||||
func (m *AgentTaskManager) CancelTask(conversationID string, cause error) (bool, error) {
|
func (m *AgentTaskManager) CancelTask(conversationID string, cause error) (bool, error) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
|
|||||||
Reference in new issue
Block a user