mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 14:07:36 +02:00
fix: preserve task cancellation through final events
This commit is contained in:
1 parent
07066f9219
commit
054a77cdab
7 files changed
+133
-4
No files matched your search
@@ -113,3 +113,9 @@
|
||||
|
||||
- Excel 第 58 行:截图的子任务为 failed,队列 completed 表示所有子任务已经结束;绿色“已完成”和 100% 被误认为全部成功。列表和详情共用展示函数,现在全部失败显示失败色及“执行失败”,部分失败显示“已结束(N 项失败)”,详情解释进度统计结束任务,包含失败/取消。后端生命周期和重跑规则不变。
|
||||
- 3 项实际展示函数回归覆盖全失败、混合结果、成功、运行中及已有周期队列的后续运行信息;不扩展调度能力。
|
||||
|
||||
## 追加:Agent 取消竞态与最终事件
|
||||
|
||||
- Excel 第 24 行:AgentTaskManager 原先允许取消之后的 UpdateTaskStatus/FinishTaskRun 覆盖终态。明确用户取消增加单向取消标记,晚到的 running/completed/failed 更新及 completed 收尾仍保存 cancelled;“中断并继续”不设置该标记。
|
||||
- 主 SSE 收尾从实际已保存任务取 status,保留原消息字段且不修改调用方对象;前端 done 不再一律覆盖为成功标题,取消/失败/超时/清理异常使用实际结果,取消时遗留工具调用标记取消。任务 eventBus 本来已有真实 status,保持不变。
|
||||
- 新增取消晚到更新、正常继续、done 实际状态与前端展示回归;完整 handler 包测试通过。Excel 第 60 行仍需审批等待中断的完整路径验证,暂不声明该历史场景全部完成。
|
||||
@@ -26,7 +26,7 @@
|
||||
| 21 | 19 | webshell 连接口令明文 | 已修复并有针对性验证 |
|
||||
| 22 | 20 | 不知道这个是啥,应该是样式问题 | 与第 25 行同一 WebShell 路径/假文件问题,已修复 |
|
||||
| 23 | 21 | 创建监听器 必填字段不清楚,交互上不知道是哪个必填 | 已修复必填标识、逐字段提示及端口严格校验;前后端回归通过 |
|
||||
| 24 | 22 | 已经取消的任务,结束之后标签会变成成功,好像没有取消成功 | C2 终态已修复,其他任务路径待核对 |
|
||||
| 24 | 22 | 已经取消的任务,结束之后标签会变成成功,好像没有取消成功 | C2 与 Agent 取消终态保护已修复,done 展示保留真实状态;审批中断场景继续核对 |
|
||||
| 25 | 23 | 【WebShell 文件管理】目录树用相对路径导致点进去必报错,且报错文本被解析成假文件条目步骤 | 已修复并有针对性验证 |
|
||||
| 26 | 24 | 工作流 id 由客户端随便填、零校验 | 已修复新 ID 格式/长度、重复创建及编辑改 ID 校验 |
|
||||
| 27 | 25 | 样式问题 | 待核对 |
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cyberstrike-ai/internal/multiagent"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCancelledAgentTaskCannotFinishSuccessfully(t *testing.T) {
|
||||
for _, lateStatus := range []string{"running", "completed", "failed"} {
|
||||
m := NewAgentTaskManager()
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
task, err := m.StartTask("conversation", "test", cancel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ok, err := m.CancelTask("conversation", ErrTaskCancelled); err != nil || !ok {
|
||||
t.Fatalf("cancel: %v %v", ok, err)
|
||||
}
|
||||
if context.Cause(ctx) != ErrTaskCancelled {
|
||||
t.Fatal("cancellation not delivered")
|
||||
}
|
||||
m.UpdateTaskStatus("conversation", lateStatus)
|
||||
if err := m.FinishTaskRun("conversation", task.RunID, "completed"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := m.GetCompletedTasks()
|
||||
if len(done) != 1 || done[0].Status != "cancelled" {
|
||||
t.Fatalf("cancelled task became successful: %#v", done)
|
||||
}
|
||||
if len(m.GetActiveTasks()) != 0 {
|
||||
t.Fatal("cancelled task remains active")
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestInterruptContinueDoesNotBecomeTaskCancellation(t *testing.T) {
|
||||
m := NewAgentTaskManager()
|
||||
_, cancel := context.WithCancelCause(context.Background())
|
||||
task, err := m.StartTask("conversation", "test", cancel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := m.CancelTask("conversation", multiagent.ErrInterruptContinue); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.UpdateTaskStatus("conversation", "running")
|
||||
if err := m.FinishTaskRun("conversation", task.RunID, "completed"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if done := m.GetCompletedTasks(); len(done) != 1 || done[0].Status != "completed" {
|
||||
t.Fatal("continue run lost success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoneEventCarriesTheSavedCancellationStatus(t *testing.T) {
|
||||
m := NewAgentTaskManager()
|
||||
task, err := m.StartTask("conversation", "test", func(error) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := m.CancelTask("conversation", ErrTaskCancelled); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := &AgentHandler{tasks: m}
|
||||
var result map[string]interface{}
|
||||
send := h.taskFinishingEventSender(func(kind, message string, data interface{}) {
|
||||
if kind == "done" {
|
||||
result = data.(map[string]interface{})
|
||||
}
|
||||
}, "conversation", task.RunID, func() string { return "completed" })
|
||||
original := map[string]interface{}{"messageId": "message"}
|
||||
send("done", "", original)
|
||||
if result["status"] != "cancelled" || result["messageId"] != "message" {
|
||||
t.Fatalf("incorrect done payload: %#v", result)
|
||||
}
|
||||
if _, exists := original["status"]; exists {
|
||||
t.Fatal("caller payload mutated")
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,22 @@ func (h *AgentHandler) taskFinishingEventSender(send func(string, string, interf
|
||||
}
|
||||
send("error", taskCleanupMessage(err)+": "+err.Error(), map[string]interface{}{"errorType": taskCleanupStatus(err)})
|
||||
data = map[string]interface{}{"conversationId": conversationID, "runId": runID, "status": taskCleanupStatus(err), "cleanupError": err.Error()}
|
||||
} else {
|
||||
payload := map[string]interface{}{}
|
||||
if existing, ok := data.(map[string]interface{}); ok {
|
||||
for key, value := range existing {
|
||||
payload[key] = value
|
||||
}
|
||||
}
|
||||
payload["conversationId"] = conversationID
|
||||
payload["runId"] = runID
|
||||
for _, task := range h.tasks.GetCompletedTasks() {
|
||||
if task.RunID == runID && task.ConversationID == conversationID {
|
||||
payload["status"] = task.Status
|
||||
break
|
||||
}
|
||||
}
|
||||
data = payload
|
||||
}
|
||||
}
|
||||
send(eventType, message, data)
|
||||
|
||||
@@ -37,6 +37,7 @@ type AgentTask struct {
|
||||
IsolationBackend string `json:"isolationBackend,omitempty"`
|
||||
finishing chan struct{}
|
||||
stopping chan struct{}
|
||||
cancelRequested bool
|
||||
finalStatus string
|
||||
ConversationID string `json:"conversationId"`
|
||||
Title string `json:"title,omitempty"`
|
||||
@@ -484,6 +485,11 @@ func (m *AgentTaskManager) CancelTask(conversationID string, cause error) (bool,
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if cause == nil || errors.Is(cause, ErrTaskCancelled) {
|
||||
task.cancelRequested = true
|
||||
task.finalStatus = "cancelled"
|
||||
}
|
||||
|
||||
// ErrInterruptContinue:仅掐断当前推理步骤,随后由处理器续跑,不进入长时间「取消中」态。
|
||||
if cause != nil && errors.Is(cause, multiagent.ErrInterruptContinue) {
|
||||
task.Status = "running"
|
||||
@@ -559,6 +565,9 @@ func (m *AgentTaskManager) UpdateTaskStatus(conversationID string, status string
|
||||
if task.finishing != nil || task.Status == "cleanup_failed" {
|
||||
return
|
||||
}
|
||||
if task.cancelRequested {
|
||||
status = "cancelled"
|
||||
}
|
||||
switch status {
|
||||
case "completed", "cancelled", "failed", "timeout":
|
||||
task.finalStatus = status
|
||||
@@ -628,6 +637,9 @@ func (m *AgentTaskManager) FinishTaskRun(conversationID, runID, finalStatus stri
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if task.cancelRequested {
|
||||
finalStatus = "cancelled"
|
||||
}
|
||||
done := make(chan struct{})
|
||||
task.finishing = done
|
||||
task.finalStatus = finalStatus
|
||||
|
||||
@@ -425,6 +425,13 @@ function getToolCallMapping(progressId, toolCallId) {
|
||||
return toolCallStatusMap.get(String(toolCallId)) || null;
|
||||
}
|
||||
|
||||
function progressDoneOutcome(data) {
|
||||
const status = String(data && (data.status || data.workflowStatus) || '').toLowerCase();
|
||||
if (status === 'cancelled' || status === 'canceled') return { key: 'chat.taskCancelled', fallback: '任务已取消', icon: '⛔', toolStatus: 'cancelled' };
|
||||
if (['failed', 'timeout', 'cleanup_failed', 'cleanup_unconfirmed'].includes(status)) return { key: status === 'timeout' ? 'tasks.statusTimeout' : 'tasks.statusFailed', fallback: status === 'timeout' ? '任务超时' : '任务执行失败', icon: '❌', toolStatus: 'failed' };
|
||||
return { key: 'chat.penetrationTestComplete', fallback: '渗透测试完成', icon: '✅', toolStatus: 'failed' };
|
||||
}
|
||||
|
||||
function finalizeOutstandingToolCallsForProgress(progressId, finalStatus) {
|
||||
if (!progressId) return;
|
||||
const pid = String(progressId);
|
||||
@@ -3866,7 +3873,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
// 立即刷新任务状态
|
||||
loadActiveTasks();
|
||||
// Close any remaining running tool calls for this progress.
|
||||
finalizeOutstandingToolCallsForProgress(progressId, 'failed');
|
||||
finalizeOutstandingToolCallsForProgress(progressId, 'cancelled');
|
||||
break;
|
||||
|
||||
case 'response_start': {
|
||||
@@ -4187,7 +4194,8 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
// 完成,更新进度标题(如果进度消息还存在)
|
||||
const doneTitle = document.querySelector(`#${progressId} .progress-stage`);
|
||||
if (doneTitle) {
|
||||
doneTitle.textContent = '✅ ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestComplete') : '渗透测试完成');
|
||||
const outcome = progressDoneOutcome(event.data);
|
||||
doneTitle.textContent = outcome.icon + ' ' + (typeof window.t === 'function' ? window.t(outcome.key) : outcome.fallback);
|
||||
}
|
||||
// 更新对话ID
|
||||
if (event.data && event.data.conversationId) {
|
||||
@@ -4207,7 +4215,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
// 立即刷新任务状态(确保任务状态同步)
|
||||
loadActiveTasks();
|
||||
// Close any remaining running tool calls for this progress (best-effort).
|
||||
finalizeOutstandingToolCallsForProgress(progressId, 'failed');
|
||||
finalizeOutstandingToolCallsForProgress(progressId, progressDoneOutcome(event.data).toolStatus);
|
||||
|
||||
// 延迟再次刷新任务状态(确保后端已完成状态更新)
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const fs=require('node:fs'),vm=require('node:vm'),test=require('node:test'),assert=require('node:assert/strict');
|
||||
const source=fs.readFileSync('web/static/js/monitor.js','utf8');const c=vm.createContext({});vm.runInContext(source.slice(source.indexOf('function progressDoneOutcome('),source.indexOf('function finalizeOutstandingToolCallsForProgress(')),c);
|
||||
test('done preserves cancelled and failed outcomes instead of showing success',()=>{
|
||||
for(const status of ['cancelled','canceled','failed','timeout','cleanup_failed','cleanup_unconfirmed']){
|
||||
const result=c.progressDoneOutcome({status});assert.notEqual(result.icon,'✅');assert.equal(result.toolStatus,status.startsWith('cancel')?'cancelled':'failed');
|
||||
}
|
||||
assert.equal(c.progressDoneOutcome({status:'completed'}).icon,'✅');assert.equal(c.progressDoneOutcome({workflowStatus:'cancelled'}).icon,'⛔');
|
||||
});
|
||||
Reference in new issue
Block a user